PageRenderTime 106ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 1ms

/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
  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();
  1127. }
  1128. }
  1129. return true;
  1130. }
  1131. /**
  1132. * Update the gradebook information for this assignment.
  1133. *
  1134. * @param bool $reset If true, will reset all grades in the gradbook for this assignment
  1135. * @param int $coursemoduleid This is required because it might not exist in the database yet
  1136. * @return bool
  1137. */
  1138. public function update_gradebook($reset, $coursemoduleid) {
  1139. global $CFG;
  1140. require_once($CFG->dirroot.'/mod/assign/lib.php');
  1141. $assign = clone $this->get_instance();
  1142. $assign->cmidnumber = $coursemoduleid;
  1143. // Set assign gradebook feedback plugin status (enabled and visible).
  1144. $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
  1145. $param = null;
  1146. if ($reset) {
  1147. $param = 'reset';
  1148. }
  1149. return assign_grade_item_update($assign, $param);
  1150. }
  1151. /**
  1152. * Get the marking table page size
  1153. *
  1154. * @return integer
  1155. */
  1156. public function get_assign_perpage() {
  1157. $perpage = (int) get_user_preferences('assign_perpage', 10);
  1158. $adminconfig = $this->get_admin_config();
  1159. $maxperpage = -1;
  1160. if (isset($adminconfig->maxperpage)) {
  1161. $maxperpage = $adminconfig->maxperpage;
  1162. }
  1163. if (isset($maxperpage) &&
  1164. $maxperpage != -1 &&
  1165. ($perpage == -1 || $perpage > $maxperpage)) {
  1166. $perpage = $maxperpage;
  1167. }
  1168. return $perpage;
  1169. }
  1170. /**
  1171. * Load and cache the admin config for this module.
  1172. *
  1173. * @return stdClass the plugin config
  1174. */
  1175. public function get_admin_config() {
  1176. if ($this->adminconfig) {
  1177. return $this->adminconfig;
  1178. }
  1179. $this->adminconfig = get_config('assign');
  1180. return $this->adminconfig;
  1181. }
  1182. /**
  1183. * Update the calendar entries for this assignment.
  1184. *
  1185. * @param int $coursemoduleid - Required to pass this in because it might
  1186. * not exist in the database yet.
  1187. * @return bool
  1188. */
  1189. public function update_calendar($coursemoduleid) {
  1190. global $DB, $CFG;
  1191. require_once($CFG->dirroot.'/calendar/lib.php');
  1192. // Special case for add_instance as the coursemodule has not been set yet.
  1193. $instance = $this->get_instance();
  1194. // Start with creating the event.
  1195. $event = new stdClass();
  1196. $event->modulename = 'assign';
  1197. $event->courseid = $instance->course;
  1198. $event->groupid = 0;
  1199. $event->userid = 0;
  1200. $event->instance = $instance->id;
  1201. $event->type = CALENDAR_EVENT_TYPE_ACTION;
  1202. // Convert the links to pluginfile. It is a bit hacky but at this stage the files
  1203. // might not have been saved in the module area yet.
  1204. $intro = $instance->intro;
  1205. if ($draftid = file_get_submitted_draft_itemid('introeditor')) {
  1206. $intro = file_rewrite_urls_to_pluginfile($intro, $draftid);
  1207. }
  1208. // We need to remove the links to files as the calendar is not ready
  1209. // to support module events with file areas.
  1210. $intro = strip_pluginfile_content($intro);
  1211. if ($this->show_intro()) {
  1212. $event->description = array(
  1213. 'text' => $intro,
  1214. 'format' => $instance->introformat
  1215. );
  1216. } else {
  1217. $event->description = array(
  1218. 'text' => '',
  1219. 'format' => $instance->introformat
  1220. );
  1221. }
  1222. $eventtype = ASSIGN_EVENT_TYPE_DUE;
  1223. if ($instance->duedate) {
  1224. $event->name = get_string('calendardue', 'assign', $instance->name);
  1225. $event->eventtype = $eventtype;
  1226. $event->timestart = $instance->duedate;
  1227. $event->timesort = $instance->duedate;
  1228. $select = "modulename = :modulename
  1229. AND instance = :instance
  1230. AND eventtype = :eventtype
  1231. AND groupid = 0
  1232. AND courseid <> 0";
  1233. $params = array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype);
  1234. $event->id = $DB->get_field_select('event', 'id', $select, $params);
  1235. // Now process the event.
  1236. if ($event->id) {
  1237. $calendarevent = calendar_event::load($event->id);
  1238. $calendarevent->update($event, false);
  1239. } else {
  1240. calendar_event::create($event, false);
  1241. }
  1242. } else {
  1243. $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
  1244. 'eventtype' => $eventtype));
  1245. }
  1246. $eventtype = ASSIGN_EVENT_TYPE_GRADINGDUE;
  1247. if ($instance->gradingduedate) {
  1248. $event->name = get_string('calendargradingdue', 'assign', $instance->name);
  1249. $event->eventtype = $eventtype;
  1250. $event->timestart = $instance->gradingduedate;
  1251. $event->timesort = $instance->gradingduedate;
  1252. $event->id = $DB->get_field('event', 'id', array('modulename' => 'assign',
  1253. 'instance' => $instance->id, 'eventtype' => $event->eventtype));
  1254. // Now process the event.
  1255. if ($event->id) {
  1256. $calendarevent = calendar_event::load($event->id);
  1257. $calendarevent->update($event, false);
  1258. } else {
  1259. calendar_event::create($event, false);
  1260. }
  1261. } else {
  1262. $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
  1263. 'eventtype' => $eventtype));
  1264. }
  1265. return true;
  1266. }
  1267. /**
  1268. * Update this instance in the database.
  1269. *
  1270. * @param stdClass $formdata - the data submitted from the form
  1271. * @return bool false if an error occurs
  1272. */
  1273. public function update_instance($formdata) {
  1274. global $DB;
  1275. $adminconfig = $this->get_admin_config();
  1276. $update = new stdClass();
  1277. $update->id = $formdata->instance;
  1278. $update->name = $formdata->name;
  1279. $update->timemodified = time();
  1280. $update->course = $formdata->course;
  1281. $update->intro = $formdata->intro;
  1282. $update->introformat = $formdata->introformat;
  1283. $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
  1284. $update->submissiondrafts = $formdata->submissiondrafts;
  1285. $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
  1286. $update->sendnotifications = $formdata->sendnotifications;
  1287. $update->sendlatenotifications = $formdata->sendlatenotifications;
  1288. $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
  1289. if (isset($formdata->sendstudentnotifications)) {
  1290. $update->sendstudentnotifications = $formdata->sendstudentnotifications;
  1291. }
  1292. $update->duedate = $formdata->duedate;
  1293. $update->cutoffdate = $formdata->cutoffdate;
  1294. $update->gradingduedate = $formdata->gradingduedate;
  1295. $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
  1296. $update->grade = $formdata->grade;
  1297. if (!empty($formdata->completionunlocked)) {
  1298. $update->completionsubmit = !empty($formdata->completionsubmit);
  1299. }
  1300. $update->teamsubmission = $formdata->teamsubmission;
  1301. $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
  1302. if (isset($formdata->teamsubmissiongroupingid)) {
  1303. $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
  1304. }
  1305. if (isset($formdata->hidegrader)) {
  1306. $update->hidegrader = $formdata->hidegrader;
  1307. }
  1308. $update->blindmarking = $formdata->blindmarking;
  1309. $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
  1310. if (!empty($formdata->attemptreopenmethod)) {
  1311. $update->attemptreopenmethod = $formdata->attemptreopenmethod;
  1312. }
  1313. if (!empty($formdata->maxattempts)) {
  1314. $update->maxattempts = $formdata->maxattempts;
  1315. }
  1316. if (isset($formdata->preventsubmissionnotingroup)) {
  1317. $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
  1318. }
  1319. $update->markingworkflow = $formdata->markingworkflow;
  1320. $update->markingallocation = $formdata->markingallocation;
  1321. if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
  1322. $update->markingallocation = 0;
  1323. }
  1324. $result = $DB->update_record('assign', $update);
  1325. $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
  1326. $this->save_intro_draft_files($formdata);
  1327. // Load the assignment so the plugins have access to it.
  1328. // Call save_settings hook for submission plugins.
  1329. foreach ($this->submissionplugins as $plugin) {
  1330. if (!$this->update_plugin_instance($plugin, $formdata)) {
  1331. print_error($plugin->get_error());
  1332. return false;
  1333. }
  1334. }
  1335. foreach ($this->feedbackplugins as $plugin) {
  1336. if (!$this->update_plugin_instance($plugin, $formdata)) {
  1337. print_error($plugin->get_error());
  1338. return false;
  1339. }
  1340. }
  1341. $this->update_calendar($this->get_course_module()->id);
  1342. $completionexpected = (!empty($formdata->completionexpected)) ? $formdata->completionexpected : null;
  1343. \core_completion\api::update_completion_date_event($this->get_course_module()->id, 'assign', $this->instance,
  1344. $completionexpected);
  1345. $this->update_gradebook(false, $this->get_course_module()->id);
  1346. $update = new stdClass();
  1347. $update->id = $this->get_instance()->id;
  1348. $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
  1349. $DB->update_record('assign', $update);
  1350. return $result;
  1351. }
  1352. /**
  1353. * Save the attachments in the draft areas.
  1354. *
  1355. * @param stdClass $formdata
  1356. */
  1357. protected function save_intro_draft_files($formdata) {
  1358. if (isset($formdata->introattachments)) {
  1359. file_save_draft_area_files($formdata->introattachments, $this->get_context()->id,
  1360. 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
  1361. }
  1362. }
  1363. /**
  1364. * Add elements in grading plugin form.
  1365. *
  1366. * @param mixed $grade stdClass|null
  1367. * @param MoodleQuickForm $mform
  1368. * @param stdClass $data
  1369. * @param int $userid - The userid we are grading
  1370. * @return void
  1371. */
  1372. protected function add_plugin_grade_elements($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
  1373. foreach ($this->feedbackplugins as $plugin) {
  1374. if ($plugin->is_enabled() && $plugin->is_visible()) {
  1375. $plugin->get_form_elements_for_user($grade, $mform, $data, $userid);
  1376. }
  1377. }
  1378. }
  1379. /**
  1380. * Add one plugins settings to edit plugin form.
  1381. *
  1382. * @param assign_plugin $plugin The plugin to add the settings from
  1383. * @param MoodleQuickForm $mform The form to add the configuration settings to.
  1384. * This form is modified directly (not returned).
  1385. * @param array $pluginsenabled A list of form elements to be added to a group.
  1386. * The new element is added to this array by this function.
  1387. * @return void
  1388. */
  1389. protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) {
  1390. global $CFG;
  1391. if ($plugin->is_visible() && !$plugin->is_configurable() && $plugin->is_enabled()) {
  1392. $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
  1393. $pluginsenabled[] = $mform->createElement('hidden', $name, 1);
  1394. $mform->setType($name, PARAM_BOOL);
  1395. $plugin->get_settings($mform);
  1396. } else if ($plugin->is_visible() && $plugin->is_configurable()) {
  1397. $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
  1398. $label = $plugin->get_name();
  1399. $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label);
  1400. $helpicon = $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type());
  1401. $pluginsenabled[] = $mform->createElement('static', '', '', $helpicon);
  1402. $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default');
  1403. if ($plugin->get_config('enabled') !== false) {
  1404. $default = $plugin->is_enabled();
  1405. }
  1406. $mform->setDefault($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled', $default);
  1407. $plugin->get_settings($mform);
  1408. }
  1409. }
  1410. /**
  1411. * Add settings to edit plugin form.
  1412. *
  1413. * @param MoodleQuickForm $mform The form to add the configuration settings to.
  1414. * This form is modified directly (not returned).
  1415. * @return void
  1416. */
  1417. public function add_all_plugin_settings(MoodleQuickForm $mform) {
  1418. $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign'));
  1419. $submissionpluginsenabled = array();
  1420. $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false);
  1421. foreach ($this->submissionplugins as $plugin) {
  1422. $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled);
  1423. }
  1424. $group->setElements($submissionpluginsenabled);
  1425. $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign'));
  1426. $feedbackpluginsenabled = array();
  1427. $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false);
  1428. foreach ($this->feedbackplugins as $plugin) {
  1429. $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled);
  1430. }
  1431. $group->setElements($feedbackpluginsenabled);
  1432. $mform->setExpanded('submissiontypes');
  1433. }
  1434. /**
  1435. * Allow each plugin an opportunity to update the defaultvalues
  1436. * passed in to the settings form (needed to set up draft areas for
  1437. * editor and filemanager elements)
  1438. *
  1439. * @param array $defaultvalues
  1440. */
  1441. public function plugin_data_preprocessing(&$defaultvalues) {
  1442. foreach ($this->submissionplugins as $plugin) {
  1443. if ($plugin->is_visible()) {
  1444. $plugin->data_preprocessing($defaultvalues);
  1445. }
  1446. }
  1447. foreach ($this->feedbackplugins as $plugin) {
  1448. if ($plugin->is_visible()) {
  1449. $plugin->data_preprocessing($defaultvalues);
  1450. }
  1451. }
  1452. }
  1453. /**
  1454. * Get the name of the current module.
  1455. *
  1456. * @return string the module name (Assignment)
  1457. */
  1458. protected function get_module_name() {
  1459. if (isset(self::$modulename)) {
  1460. return self::$modulename;
  1461. }
  1462. self::$modulename = get_string('modulename', 'assign');
  1463. return self::$modulename;
  1464. }
  1465. /**
  1466. * Get the plural name of the current module.
  1467. *
  1468. * @return string the module name plural (Assignments)
  1469. */
  1470. protected function get_module_name_plural() {
  1471. if (isset(self::$modulenameplural)) {
  1472. return self::$modulenameplural;
  1473. }
  1474. self::$modulenameplural = get_string('modulenameplural', 'assign');
  1475. return self::$modulenameplural;
  1476. }
  1477. /**
  1478. * Has this assignment been constructed from an instance?
  1479. *
  1480. * @return bool
  1481. */
  1482. public function has_instance() {
  1483. return $this->instance || $this->get_course_module();
  1484. }
  1485. /**
  1486. * Get the settings for the current instance of this assignment.
  1487. *
  1488. * @return stdClass The settings
  1489. * @throws dml_exception
  1490. */
  1491. public function get_default_instance() {
  1492. global $DB;
  1493. if (!$this->instance && $this->get_course_module()) {
  1494. $params = array('id' => $this->get_course_module()->instance);
  1495. $this->instance = $DB->get_record('assign', $params, '*', MUST_EXIST);
  1496. $this->userinstances = [];
  1497. }
  1498. return $this->instance;
  1499. }
  1500. /**
  1501. * Get the settings for the current instance of this assignment
  1502. * @param int|null $userid the id of the user to load the assign instance for.
  1503. * @return stdClass The settings
  1504. */
  1505. public function get_instance(int $userid = null) : stdClass {
  1506. global $USER;
  1507. $userid = $userid ?? $USER->id;
  1508. $this->instance = $this->get_default_instance();
  1509. // If we have the user instance already, just return it.
  1510. if (isset($this->userinstances[$userid])) {
  1511. return $this->userinstances[$userid];
  1512. }
  1513. // Calculate properties which vary per user.
  1514. $this->userinstances[$userid] = $this->calculate_properties($this->instance, $userid);
  1515. return $this->userinstances[$userid];
  1516. }
  1517. /**
  1518. * Calculates and updates various properties based on the specified user.
  1519. *
  1520. * @param stdClass $record the raw assign record.
  1521. * @param int $userid the id of the user to calculate the properties for.
  1522. * @return stdClass a new record having calculated properties.
  1523. */
  1524. private function calculate_properties(\stdClass $record, int $userid) : \stdClass {
  1525. $record = clone ($record);
  1526. // Relative dates.
  1527. if (!empty($record->duedate)) {
  1528. $course = $this->get_course();
  1529. $usercoursedates = course_get_course_dates_for_user_id($course, $userid);
  1530. if ($usercoursedates['start']) {
  1531. $userprops = ['duedate' => $record->duedate + $usercoursedates['startoffset']];
  1532. $record = (object) array_merge((array) $record, (array) $userprops);
  1533. }
  1534. }
  1535. return $record;
  1536. }
  1537. /**
  1538. * Get the primary grade item for this assign instance.
  1539. *
  1540. * @return grade_item The grade_item record
  1541. */
  1542. public function get_grade_item() {
  1543. if ($this->gradeitem) {
  1544. return $this->gradeitem;
  1545. }
  1546. $instance = $this->get_instance();
  1547. $params = array('itemtype' => 'mod',
  1548. 'itemmodule' => 'assign',
  1549. 'iteminstance' => $instance->id,
  1550. 'courseid' => $instance->course,
  1551. 'itemnumber' => 0);
  1552. $this->gradeitem = grade_item::fetch($params);
  1553. if (!$this->gradeitem) {
  1554. throw new coding_exception('Improper use of the assignment class. ' .
  1555. 'Cannot load the grade item.');
  1556. }
  1557. return $this->gradeitem;
  1558. }
  1559. /**
  1560. * Get the context of the current course.
  1561. *
  1562. * @return mixed context|null The course context
  1563. */
  1564. public function get_course_context() {
  1565. if (!$this->context && !$this->course) {
  1566. throw new coding_exception('Improper use of the assignment class. ' .
  1567. 'Cannot load the course context.');
  1568. }
  1569. if ($this->context) {
  1570. return $this->context->get_course_context();
  1571. } else {
  1572. return context_course::instance($this->course->id);
  1573. }
  1574. }
  1575. /**
  1576. * Get the current course module.
  1577. *
  1578. * @return cm_info|null The course module or null if not known
  1579. */
  1580. public function get_course_module() {
  1581. if ($this->coursemodule) {
  1582. return $this->coursemodule;
  1583. }
  1584. if (!$this->context) {
  1585. return null;
  1586. }
  1587. if ($this->context->contextlevel == CONTEXT_MODULE) {
  1588. $modinfo = get_fast_modinfo($this->get_course());
  1589. $this->coursemodule = $modinfo->get_cm($this->context->instanceid);
  1590. return $this->coursemodule;
  1591. }
  1592. return null;
  1593. }
  1594. /**
  1595. * Get context module.
  1596. *
  1597. * @return context
  1598. */
  1599. public function get_context() {
  1600. return $this->context;
  1601. }
  1602. /**
  1603. * Get the current course.
  1604. *
  1605. * @return mixed stdClass|null The course
  1606. */
  1607. public function get_course() {
  1608. global $DB;
  1609. if ($this->course && is_object($this->course)) {
  1610. return $this->course;
  1611. }
  1612. if (!$this->context) {
  1613. return null;
  1614. }
  1615. $params = array('id' => $this->get_course_context()->instanceid);
  1616. $this->course = $DB->get_record('course', $params, '*', MUST_EXIST);
  1617. return $this->course;
  1618. }
  1619. /**
  1620. * Count the number of intro attachments.
  1621. *
  1622. * @return int
  1623. */
  1624. protected function count_attachments() {
  1625. $fs = get_file_storage();
  1626. $files = $fs->get_area_files($this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA,
  1627. 0, 'id', false);
  1628. return count($files);
  1629. }
  1630. /**
  1631. * Are there any intro attachments to display?
  1632. *
  1633. * @return boolean
  1634. */
  1635. protected function has_visible_attachments() {
  1636. return ($this->count_attachments() > 0);
  1637. }
  1638. /**
  1639. * Return a grade in user-friendly form, whether it's a scale or not.
  1640. *
  1641. * @param mixed $grade int|null
  1642. * @param boolean $editing Are we allowing changes to this grade?
  1643. * @param int $userid The user id the grade belongs to
  1644. * @param int $modified Timestamp from when the grade was last modified
  1645. * @return string User-friendly representation of grade
  1646. */
  1647. public function display_grade($grade, $editing, $userid=0, $modified=0) {
  1648. global $DB;
  1649. static $scalegrades = array();
  1650. $o = '';
  1651. if ($this->get_instance()->grade >= 0) {
  1652. // Normal number.
  1653. if ($editing && $this->get_instance()->grade > 0) {
  1654. if ($grade < 0) {
  1655. $displaygrade = '';
  1656. } else {
  1657. $displaygrade = format_float($grade, $this->get_grade_item()->get_decimals());
  1658. }
  1659. $o .= '<label class="accesshide" for="quickgrade_' . $userid . '">' .
  1660. get_string('usergrade', 'assign') .
  1661. '</label>';
  1662. $o .= '<input type="text"
  1663. id="quickgrade_' . $userid . '"
  1664. name="quickgrade_' . $userid . '"
  1665. value="' . $displaygrade . '"
  1666. size="6"
  1667. maxlength="10"
  1668. class="quickgrade"/>';
  1669. $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $this->get_grade_item()->get_decimals());
  1670. return $o;
  1671. } else {
  1672. if ($grade == -1 || $grade === null) {
  1673. $o .= '-';
  1674. } else {
  1675. $item = $this->get_grade_item();
  1676. $o .= grade_format_gradevalue($grade, $item);
  1677. if ($item->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
  1678. // If displaying the raw grade, also display the total value.
  1679. $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $item->get_decimals());
  1680. }
  1681. }
  1682. return $o;
  1683. }
  1684. } else {
  1685. // Scale.
  1686. if (empty($this->cache['scale'])) {
  1687. if ($scale = $DB->get_record('scale', array('id'=>-($this->get_instance()->grade)))) {
  1688. $this->cache['scale'] = make_menu_from_list($scale->scale);
  1689. } else {
  1690. $o .= '-';
  1691. return $o;
  1692. }
  1693. }
  1694. if ($editing) {
  1695. $o .= '<label class="accesshide"
  1696. for="quickgrade_' . $userid . '">' .
  1697. get_string('usergrade', 'assign') .
  1698. '</label>';
  1699. $o .= '<select name="quickgrade_' . $userid . '" class="quickgrade">';
  1700. $o .= '<option value="-1">' . get_string('nograde') . '</option>';
  1701. foreach ($this->cache['scale'] as $optionid => $option) {
  1702. $selected = '';
  1703. if ($grade == $optionid) {
  1704. $selected = 'selected="selected"';
  1705. }
  1706. $o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>';
  1707. }
  1708. $o .= '</select>';
  1709. return $o;
  1710. } else {
  1711. $scaleid = (int)$grade;
  1712. if (isset($this->cache['scale'][$scaleid])) {
  1713. $o .= $this->cache['scale'][$scaleid];
  1714. return $o;
  1715. }
  1716. $o .= '-';
  1717. return $o;
  1718. }
  1719. }
  1720. }
  1721. /**
  1722. * Get the submission status/grading status for all submissions in this assignment for the
  1723. * given paticipants.
  1724. *
  1725. * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
  1726. * If this is a group assignment, group info is also returned.
  1727. *
  1728. * @param array $participants an associative array where the key is the participant id and
  1729. * the value is the participant record.
  1730. * @return array an associative array where the key is the participant id and the value is
  1731. * the participant record.
  1732. */
  1733. private function get_submission_info_for_participants($participants) {
  1734. global $DB;
  1735. if (empty($participants)) {
  1736. return $participants;
  1737. }
  1738. list($insql, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
  1739. $assignid = $this->get_instance()->id;
  1740. $params['assignmentid1'] = $assignid;
  1741. $params['assignmentid2'] = $assignid;
  1742. $params['assignmentid3'] = $assignid;
  1743. $fields = 'SELECT u.id, s.status, s.timemodified AS stime, g.timemodified AS gtime, g.grade, uf.extensionduedate';
  1744. $from = ' FROM {user} u
  1745. LEFT JOIN {assign_submission} s
  1746. ON u.id = s.userid
  1747. AND s.assignment = :assignmentid1
  1748. AND s.latest = 1
  1749. LEFT JOIN {assign_grades} g
  1750. ON u.id = g.userid
  1751. AND g.assignment = :assignmentid2
  1752. AND g.attemptnumber = s.attemptnumber
  1753. LEFT JOIN {assign_user_flags} uf
  1754. ON u.id = uf.userid
  1755. AND uf.assignment = :assignmentid3
  1756. ';
  1757. $where = ' WHERE u.id ' . $insql;
  1758. if (!empty($this->get_instance()->blindmarking)) {
  1759. $from .= 'LEFT JOIN {assign_user_mapping} um
  1760. ON u.id = um.userid
  1761. AND um.assignment = :assignmentid4 ';
  1762. $params['assignmentid4'] = $assignid;
  1763. $fields .= ', um.id as recordid ';
  1764. }
  1765. $sql = "$fields $from $where";
  1766. $records = $DB->get_records_sql($sql, $params);
  1767. if ($this->get_instance()->teamsubmission) {
  1768. // Get all groups.
  1769. $allgroups = groups_get_all_groups($this->get_course()->id,
  1770. array_keys($participants),
  1771. $this->get_instance()->teamsubmissiongroupingid,
  1772. 'DISTINCT g.id, g.name');
  1773. }
  1774. foreach ($participants as $userid => $participant) {
  1775. $participants[$userid]->fullname = $this->fullname($participant);
  1776. $participants[$userid]->submitted = false;
  1777. $participants[$userid]->requiregrading = false;
  1778. $participants[$userid]->grantedextension = false;
  1779. }
  1780. foreach ($records as $userid => $submissioninfo) {
  1781. // These filters are 100% the same as the ones in the grading table SQL.
  1782. $submitted = false;
  1783. $requiregrading = false;
  1784. $grantedextension = false;
  1785. if (!empty($submissioninfo->stime) && $submissioninfo->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
  1786. $submitted = true;
  1787. }
  1788. if ($submitted && ($submissioninfo->stime >= $submissioninfo->gtime ||
  1789. empty($submissioninfo->gtime) ||
  1790. $submissioninfo->grade === null)) {
  1791. $requiregrading = true;
  1792. }
  1793. if (!empty($submissioninfo->extensionduedate)) {
  1794. $grantedextension = true;
  1795. }
  1796. $participants[$userid]->submitted = $submitted;
  1797. $participants[$userid]->requiregrading = $requiregrading;
  1798. $participants[$userid]->grantedextension = $grantedextension;
  1799. if ($this->get_instance()->teamsubmission) {
  1800. $group = $this->get_submission_group($userid);
  1801. if ($group) {
  1802. $participants[$userid]->groupid = $group->id;
  1803. $participants[$userid]->groupname = $group->name;
  1804. }
  1805. }
  1806. }
  1807. return $participants;
  1808. }
  1809. /**
  1810. * Get the submission status/grading status for all submissions in this assignment.
  1811. * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
  1812. * If this is a group assignment, group info is also returned.
  1813. *
  1814. * @param int $currentgroup
  1815. * @param boolean $tablesort Apply current user table sorting preferences.
  1816. * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'grantedextension',
  1817. * 'groupid', 'groupname'
  1818. */
  1819. public function list_participants_with_filter_status_and_group($currentgroup, $tablesort = false) {
  1820. $participants = $this->list_participants($currentgroup, false, $tablesort);
  1821. if (empty($participants)) {
  1822. return $participants;
  1823. } else {
  1824. return $this->get_submission_info_for_participants($participants);
  1825. }
  1826. }
  1827. /**
  1828. * Return a valid order by segment for list_participants that matches
  1829. * the sorting of the current grading table. Not every field is supported,
  1830. * we are only concerned with a list of users so we can't search on anything
  1831. * that is not part of the user information (like grading statud or last modified stuff).
  1832. *
  1833. * @return string Order by clause for list_participants
  1834. */
  1835. private function get_grading_sort_sql() {
  1836. $usersort = flexible_table::get_sort_for_table('mod_assign_grading');
  1837. // TODO Does not support custom user profile fields (MDL-70456).
  1838. $userfieldsapi = \core_user\fields::for_identity($this->context, false)->with_userpic();
  1839. $userfields = $userfieldsapi->get_required_fields();
  1840. $orderfields = explode(',', $usersort);
  1841. $validlist = [];
  1842. foreach ($orderfields as $orderfield) {
  1843. $orderfield = trim($orderfield);
  1844. foreach ($userfields as $field) {
  1845. $parts = explode(' ', $orderfield);
  1846. if ($parts[0] == $field) {
  1847. // Prepend the user table prefix and count this as a valid order field.
  1848. array_push($validlist, 'u.' . $orderfield);
  1849. }
  1850. }
  1851. }
  1852. // Produce a final list.
  1853. $result = implode(',', $validlist);
  1854. if (empty($result)) {
  1855. // Fall back ordering when none has been set.
  1856. $result = 'u.lastname, u.firstname, u.id';
  1857. }
  1858. return $result;
  1859. }
  1860. /**
  1861. * Returns array with sql code and parameters returning all ids of users who have submitted an assignment.
  1862. *
  1863. * @param int $group The group that the query is for.
  1864. * @return array list($sql, $params)
  1865. */
  1866. protected function get_submitted_sql($group = 0) {
  1867. // We need to guarentee unique table names.
  1868. static $i = 0;
  1869. $i++;
  1870. $prefix = 'sa' . $i . '_';
  1871. $params = [
  1872. "{$prefix}assignment" => (int) $this->get_instance()->id,
  1873. "{$prefix}status" => ASSIGN_SUBMISSION_STATUS_NEW,
  1874. ];
  1875. $capjoin = get_enrolled_with_capabilities_join($this->context, $prefix, '', $group, $this->show_only_active_users());
  1876. $params += $capjoin->params;
  1877. $sql = "SELECT {$prefix}s.userid
  1878. FROM {assign_submission} {$prefix}s
  1879. JOIN {user} {$prefix}u ON {$prefix}u.id = {$prefix}s.userid
  1880. $capjoin->joins
  1881. WHERE {$prefix}s.assignment = :{$prefix}assignment
  1882. AND {$prefix}s.status <> :{$prefix}status
  1883. AND $capjoin->wheres";
  1884. return array($sql, $params);
  1885. }
  1886. /**
  1887. * Load a list of users enrolled in the current course with the specified permission and group.
  1888. * 0 for no group.
  1889. * Apply any current sort filters from the grading table.
  1890. *
  1891. * @param int $currentgroup
  1892. * @param bool $idsonly
  1893. * @param bool $tablesort
  1894. * @return array List of user records
  1895. */
  1896. public function list_participants($currentgroup, $idsonly, $tablesort = false) {
  1897. global $DB, $USER;
  1898. // Get the last known sort order for the grading table.
  1899. if (empty($currentgroup)) {
  1900. $currentgroup = 0;
  1901. }
  1902. $key = $this->context->id . '-' . $currentgroup . '-' . $this->show_only_active_users();
  1903. if (!isset($this->participants[$key])) {
  1904. list($esql, $params) = get_enrolled_sql($this->context, 'mod/assign:submit', $currentgroup,
  1905. $this->show_only_active_users());
  1906. list($ssql, $sparams) = $this->get_submitted_sql($currentgroup);
  1907. $params += $sparams;
  1908. $fields = 'u.*';
  1909. $orderby = 'u.lastname, u.firstname, u.id';
  1910. $additionaljoins = '';
  1911. $additionalfilters = '';
  1912. $instance = $this->get_instance();
  1913. if (!empty($instance->blindmarking)) {
  1914. $additionaljoins .= " LEFT JOIN {assign_user_mapping} um
  1915. ON u.id = um.userid
  1916. AND um.assignment = :assignmentid1
  1917. LEFT JOIN {assign_submission} s
  1918. ON u.id = s.userid
  1919. AND s.assignment = :assignmentid2
  1920. AND s.latest = 1
  1921. ";
  1922. $params['assignmentid1'] = (int) $instance->id;
  1923. $params['assignmentid2'] = (int) $instance->id;
  1924. $fields .= ', um.id as recordid ';
  1925. // Sort by submission time first, then by um.id to sort reliably by the blind marking id.
  1926. // Note, different DBs have different ordering of NULL values.
  1927. // Therefore we coalesce the current time into the timecreated field, and the max possible integer into
  1928. // the ID field.
  1929. if (empty($tablesort)) {
  1930. $orderby = "COALESCE(s.timecreated, " . time() . ") ASC, COALESCE(s.id, " . PHP_INT_MAX . ") ASC, um.id ASC";
  1931. }
  1932. }
  1933. if ($instance->markingworkflow &&
  1934. $instance->markingallocation &&
  1935. !has_capability('mod/assign:manageallocations', $this->get_context()) &&
  1936. has_capability('mod/assign:grade', $this->get_context())) {
  1937. $additionaljoins .= ' LEFT JOIN {assign_user_flags} uf
  1938. ON u.id = uf.userid
  1939. AND uf.assignment = :assignmentid3';
  1940. $params['assignmentid3'] = (int) $instance->id;
  1941. $additionalfilters .= ' AND uf.allocatedmarker = :markerid';
  1942. $params['markerid'] = $USER->id;
  1943. }
  1944. $sql = "SELECT $fields
  1945. FROM {user} u
  1946. JOIN ($esql UNION $ssql) je ON je.id = u.id
  1947. $additionaljoins
  1948. WHERE u.deleted = 0
  1949. $additionalfilters
  1950. ORDER BY $orderby";
  1951. $users = $DB->get_records_sql($sql, $params);
  1952. $cm = $this->get_course_module();
  1953. $info = new \core_availability\info_module($cm);
  1954. $users = $info->filter_user_list($users);
  1955. $this->participants[$key] = $users;
  1956. }
  1957. if ($tablesort) {
  1958. // Resort the user list according to the grading table sort and filter settings.
  1959. $sortedfiltereduserids = $this->get_grading_userid_list(true, '');
  1960. $sortedfilteredusers = [];
  1961. foreach ($sortedfiltereduserids as $nextid) {
  1962. $nextid = intval($nextid);
  1963. if (isset($this->participants[$key][$nextid])) {
  1964. $sortedfilteredusers[$nextid] = $this->participants[$key][$nextid];
  1965. }
  1966. }
  1967. $this->participants[$key] = $sortedfilteredusers;
  1968. }
  1969. if ($idsonly) {
  1970. $idslist = array();
  1971. foreach ($this->participants[$key] as $id => $user) {
  1972. $idslist[$id] = new stdClass();
  1973. $idslist[$id]->id = $id;
  1974. }
  1975. return $idslist;
  1976. }
  1977. return $this->participants[$key];
  1978. }
  1979. /**
  1980. * Load a user if they are enrolled in the current course. Populated with submission
  1981. * status for this assignment.
  1982. *
  1983. * @param int $userid
  1984. * @return null|stdClass user record
  1985. */
  1986. public function get_participant($userid) {
  1987. global $DB, $USER;
  1988. if ($userid == $USER->id) {
  1989. $participant = clone ($USER);
  1990. } else {
  1991. $participant = $DB->get_record('user', array('id' => $userid));
  1992. }
  1993. if (!$participant) {
  1994. return null;
  1995. }
  1996. if (!is_enrolled($this->context, $participant, '', $this->show_only_active_users())) {
  1997. return null;
  1998. }
  1999. $result = $this->get_submission_info_for_participants(array($participant->id => $participant));
  2000. $submissioninfo = $result[$participant->id];
  2001. if (!$submissioninfo->submitted && !has_capability('mod/assign:submit', $this->context, $userid)) {
  2002. return null;
  2003. }
  2004. return $submissioninfo;
  2005. }
  2006. /**
  2007. * Load a count of valid teams for this assignment.
  2008. *
  2009. * @param int $activitygroup Activity active group
  2010. * @return int number of valid teams
  2011. */
  2012. public function count_teams($activitygroup = 0) {
  2013. $count = 0;
  2014. $participants = $this->list_participants($activitygroup, true);
  2015. // If a team submission grouping id is provided all good as all returned groups
  2016. // are the submission teams, but if no team submission grouping was specified
  2017. // $groups will contain all participants groups.
  2018. if ($this->get_instance()->teamsubmissiongroupingid) {
  2019. // We restrict the users to the selected group ones.
  2020. $groups = groups_get_all_groups($this->get_course()->id,
  2021. array_keys($participants),
  2022. $this->get_instance()->teamsubmissiongroupingid,
  2023. 'DISTINCT g.id, g.name');
  2024. $count = count($groups);
  2025. // When a specific group is selected we don't count the default group users.
  2026. if ($activitygroup == 0) {
  2027. if (empty($this->get_instance()->preventsubmissionnotingroup)) {
  2028. // See if there are any users in the default group.
  2029. $defaultusers = $this->get_submission_group_members(0, true);
  2030. if (count($defaultusers) > 0) {
  2031. $count += 1;
  2032. }
  2033. }
  2034. } else if ($activitygroup != 0 && empty($groups)) {
  2035. // Set count to 1 if $groups returns empty.
  2036. // It means the group is not part of $this->get_instance()->teamsubmissiongroupingid.
  2037. $count = 1;
  2038. }
  2039. } else {
  2040. // It is faster to loop around participants if no grouping was specified.
  2041. $groups = array();
  2042. foreach ($participants as $participant) {
  2043. if ($group = $this->get_submission_group($participant->id)) {
  2044. $groups[$group->id] = true;
  2045. } else if (empty($this->get_instance()->preventsubmissionnotingroup)) {
  2046. $groups[0] = true;
  2047. }
  2048. }
  2049. $count = count($groups);
  2050. }
  2051. return $count;
  2052. }
  2053. /**
  2054. * Load a count of active users enrolled in the current course with the specified permission and group.
  2055. * 0 for no group.
  2056. *
  2057. * @param int $currentgroup
  2058. * @return int number of matching users
  2059. */
  2060. public function count_participants($currentgroup) {
  2061. return count($this->list_participants($currentgroup, true));
  2062. }
  2063. /**
  2064. * Load a count of active users submissions in the current module that require grading
  2065. * This means the submission modification time is more recent than the
  2066. * grading modification time and the status is SUBMITTED.
  2067. *
  2068. * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
  2069. * @return int number of matching submissions
  2070. */
  2071. public function count_submissions_need_grading($currentgroup = null) {
  2072. global $DB;
  2073. if ($this->get_instance()->teamsubmission) {
  2074. // This does not make sense for group assignment because the submission is shared.
  2075. return 0;
  2076. }
  2077. if ($currentgroup === null) {
  2078. $currentgroup = groups_get_activity_group($this->get_course_module(), true);
  2079. }
  2080. list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true);
  2081. $params['assignid'] = $this->get_instance()->id;
  2082. $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
  2083. $sqlscalegrade = $this->get_instance()->grade < 0 ? ' OR g.grade = -1' : '';
  2084. $sql = 'SELECT COUNT(s.userid)
  2085. FROM {assign_submission} s
  2086. LEFT JOIN {assign_grades} g ON
  2087. s.assignment = g.assignment AND
  2088. s.userid = g.userid AND
  2089. g.attemptnumber = s.attemptnumber
  2090. JOIN(' . $esql . ') e ON e.id = s.userid
  2091. WHERE
  2092. s.latest = 1 AND
  2093. s.assignment = :assignid AND
  2094. s.timemodified IS NOT NULL AND
  2095. s.status = :submitted AND
  2096. (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL '
  2097. . $sqlscalegrade . ')';
  2098. return $DB->count_records_sql($sql, $params);
  2099. }
  2100. /**
  2101. * Load a count of grades.
  2102. *
  2103. * @return int number of grades
  2104. */
  2105. public function count_grades() {
  2106. global $DB;
  2107. if (!$this->has_instance()) {
  2108. return 0;
  2109. }
  2110. $currentgroup = groups_get_activity_group($this->get_course_module(), true);
  2111. list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
  2112. $params['assignid'] = $this->get_instance()->id;
  2113. $sql = 'SELECT COUNT(g.userid)
  2114. FROM {assign_grades} g
  2115. JOIN(' . $esql . ') e ON e.id = g.userid
  2116. WHERE g.assignment = :assignid';
  2117. return $DB->count_records_sql($sql, $params);
  2118. }
  2119. /**
  2120. * Load a count of submissions.
  2121. *
  2122. * @param bool $includenew When true, also counts the submissions with status 'new'.
  2123. * @return int number of submissions
  2124. */
  2125. public function count_submissions($includenew = false) {
  2126. global $DB;
  2127. if (!$this->has_instance()) {
  2128. return 0;
  2129. }
  2130. $params = array();
  2131. $sqlnew = '';
  2132. if (!$includenew) {
  2133. $sqlnew = ' AND s.status <> :status ';
  2134. $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW;
  2135. }
  2136. if ($this->get_instance()->teamsubmission) {
  2137. // We cannot join on the enrolment tables for group submissions (no userid).
  2138. $sql = 'SELECT COUNT(DISTINCT s.groupid)
  2139. FROM {assign_submission} s
  2140. WHERE
  2141. s.assignment = :assignid AND
  2142. s.timemodified IS NOT NULL AND
  2143. s.userid = :groupuserid' .
  2144. $sqlnew;
  2145. $params['assignid'] = $this->get_instance()->id;
  2146. $params['groupuserid'] = 0;
  2147. } else {
  2148. $currentgroup = groups_get_activity_group($this->get_course_module(), true);
  2149. list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
  2150. $params = array_merge($params, $enrolparams);
  2151. $params['assignid'] = $this->get_instance()->id;
  2152. $sql = 'SELECT COUNT(DISTINCT s.userid)
  2153. FROM {assign_submission} s
  2154. JOIN(' . $esql . ') e ON e.id = s.userid
  2155. WHERE
  2156. s.assignment = :assignid AND
  2157. s.timemodified IS NOT NULL ' .
  2158. $sqlnew;
  2159. }
  2160. return $DB->count_records_sql($sql, $params);
  2161. }
  2162. /**
  2163. * Load a count of submissions with a specified status.
  2164. *
  2165. * @param string $status The submission status - should match one of the constants
  2166. * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
  2167. * @return int number of matching submissions
  2168. */
  2169. public function count_submissions_with_status($status, $currentgroup = null) {
  2170. global $DB;
  2171. if ($currentgroup === null) {
  2172. $currentgroup = groups_get_activity_group($this->get_course_module(), true);
  2173. }
  2174. list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true);
  2175. $params['assignid'] = $this->get_instance()->id;
  2176. $params['assignid2'] = $this->get_instance()->id;
  2177. $params['submissionstatus'] = $status;
  2178. if ($this->get_instance()->teamsubmission) {
  2179. $groupsstr = '';
  2180. if ($currentgroup != 0) {
  2181. // If there is an active group we should only display the current group users groups.
  2182. $participants = $this->list_participants($currentgroup, true);
  2183. $groups = groups_get_all_groups($this->get_course()->id,
  2184. array_keys($participants),
  2185. $this->get_instance()->teamsubmissiongroupingid,
  2186. 'DISTINCT g.id, g.name');
  2187. if (empty($groups)) {
  2188. // If $groups is empty it means it is not part of $this->get_instance()->teamsubmissiongroupingid.
  2189. // All submissions from students that do not belong to any of teamsubmissiongroupingid groups
  2190. // count towards groupid = 0. Setting to true as only '0' key matters.
  2191. $groups = [true];
  2192. }
  2193. list($groupssql, $groupsparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED);
  2194. $groupsstr = 's.groupid ' . $groupssql . ' AND';
  2195. $params = $params + $groupsparams;
  2196. }
  2197. $sql = 'SELECT COUNT(s.groupid)
  2198. FROM {assign_submission} s
  2199. WHERE
  2200. s.latest = 1 AND
  2201. s.assignment = :assignid AND
  2202. s.timemodified IS NOT NULL AND
  2203. s.userid = :groupuserid AND '
  2204. . $groupsstr . '
  2205. s.status = :submissionstatus';
  2206. $params['groupuserid'] = 0;
  2207. } else {
  2208. $sql = 'SELECT COUNT(s.userid)
  2209. FROM {assign_submission} s
  2210. JOIN(' . $esql . ') e ON e.id = s.userid
  2211. WHERE
  2212. s.latest = 1 AND
  2213. s.assignment = :assignid AND
  2214. s.timemodified IS NOT NULL AND
  2215. s.status = :submissionstatus';
  2216. }
  2217. return $DB->count_records_sql($sql, $params);
  2218. }
  2219. /**
  2220. * Utility function to get the userid for every row in the grading table
  2221. * so the order can be frozen while we iterate it.
  2222. *
  2223. * @param boolean $cached If true, the cached list from the session could be returned.
  2224. * @param string $useridlistid String value used for caching the participant list.
  2225. * @return array An array of userids
  2226. */
  2227. protected function get_grading_userid_list($cached = false, $useridlistid = '') {
  2228. global $SESSION;
  2229. if ($cached) {
  2230. if (empty($useridlistid)) {
  2231. $useridlistid = $this->get_useridlist_key_id();
  2232. }
  2233. $useridlistkey = $this->get_useridlist_key($useridlistid);
  2234. if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) {
  2235. $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list(false, '');
  2236. }
  2237. return $SESSION->mod_assign_useridlist[$useridlistkey];
  2238. }
  2239. $filter = get_user_preferences('assign_filter', '');
  2240. $table = new assign_grading_table($this, 0, $filter, 0, false);
  2241. $useridlist = $table->get_column_data('userid');
  2242. return $useridlist;
  2243. }
  2244. /**
  2245. * Finds all assignment notifications that have yet to be mailed out, and mails them.
  2246. *
  2247. * Cron function to be run periodically according to the moodle cron.
  2248. *
  2249. * @return bool
  2250. */
  2251. public static function cron() {
  2252. global $DB;
  2253. // Only ever send a max of one days worth of updates.
  2254. $yesterday = time() - (24 * 3600);
  2255. $timenow = time();
  2256. $task = \core\task\manager::get_scheduled_task(mod_assign\task\cron_task::class);
  2257. $lastruntime = $task->get_last_run_time();
  2258. // Collect all submissions that require mailing.
  2259. // Submissions are included if all are true:
  2260. // - The assignment is visible in the gradebook.
  2261. // - No previous notification has been sent.
  2262. // - The grader was a real user, not an automated process.
  2263. // - The grade was updated in the past 24 hours.
  2264. // - If marking workflow is enabled, the workflow state is at 'released'.
  2265. $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities, a.hidegrader,
  2266. g.*, g.timemodified as lastmodified, cm.id as cmid, um.id as recordid
  2267. FROM {assign} a
  2268. JOIN {assign_grades} g ON g.assignment = a.id
  2269. LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid
  2270. JOIN {course_modules} cm ON cm.course = a.course AND cm.instance = a.id
  2271. JOIN {modules} md ON md.id = cm.module AND md.name = 'assign'
  2272. JOIN {grade_items} gri ON gri.iteminstance = a.id AND gri.courseid = a.course AND gri.itemmodule = md.name
  2273. LEFT JOIN {assign_user_mapping} um ON g.id = um.userid AND um.assignment = a.id
  2274. WHERE (a.markingworkflow = 0 OR (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND
  2275. g.grader > 0 AND uf.mailed = 0 AND gri.hidden = 0 AND
  2276. g.timemodified >= :yesterday AND g.timemodified <= :today
  2277. ORDER BY a.course, cm.id";
  2278. $params = array(
  2279. 'yesterday' => $yesterday,
  2280. 'today' => $timenow,
  2281. 'wfreleased' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
  2282. );
  2283. $submissions = $DB->get_records_sql($sql, $params);
  2284. if (!empty($submissions)) {
  2285. mtrace('Processing ' . count($submissions) . ' assignment submissions ...');
  2286. // Preload courses we are going to need those.
  2287. $courseids = array();
  2288. foreach ($submissions as $submission) {
  2289. $courseids[] = $submission->course;
  2290. }
  2291. // Filter out duplicates.
  2292. $courseids = array_unique($courseids);
  2293. $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
  2294. list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
  2295. $sql = 'SELECT c.*, ' . $ctxselect .
  2296. ' FROM {course} c
  2297. LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
  2298. WHERE c.id ' . $courseidsql;
  2299. $params['contextlevel'] = CONTEXT_COURSE;
  2300. $courses = $DB->get_records_sql($sql, $params);
  2301. // Clean up... this could go on for a while.
  2302. unset($courseids);
  2303. unset($ctxselect);
  2304. unset($courseidsql);
  2305. unset($params);
  2306. // Message students about new feedback.
  2307. foreach ($submissions as $submission) {
  2308. mtrace("Processing assignment submission $submission->id ...");
  2309. // Do not cache user lookups - could be too many.
  2310. if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) {
  2311. mtrace('Could not find user ' . $submission->userid);
  2312. continue;
  2313. }
  2314. // Use a cache to prevent the same DB queries happening over and over.
  2315. if (!array_key_exists($submission->course, $courses)) {
  2316. mtrace('Could not find course ' . $submission->course);
  2317. continue;
  2318. }
  2319. $course = $courses[$submission->course];
  2320. if (isset($course->ctxid)) {
  2321. // Context has not yet been preloaded. Do so now.
  2322. context_helper::preload_from_record($course);
  2323. }
  2324. // Override the language and timezone of the "current" user, so that
  2325. // mail is customised for the receiver.
  2326. cron_setup_user($user, $course);
  2327. // Context lookups are already cached.
  2328. $coursecontext = context_course::instance($course->id);
  2329. if (!is_enrolled($coursecontext, $user->id)) {
  2330. $courseshortname = format_string($course->shortname,
  2331. true,
  2332. array('context' => $coursecontext));
  2333. mtrace(fullname($user) . ' not an active participant in ' . $courseshortname);
  2334. continue;
  2335. }
  2336. if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) {
  2337. mtrace('Could not find grader ' . $submission->grader);
  2338. continue;
  2339. }
  2340. $modinfo = get_fast_modinfo($course, $user->id);
  2341. $cm = $modinfo->get_cm($submission->cmid);
  2342. // Context lookups are already cached.
  2343. $contextmodule = context_module::instance($cm->id);
  2344. if (!$cm->uservisible) {
  2345. // Hold mail notification for assignments the user cannot access until later.
  2346. continue;
  2347. }
  2348. // Notify the student. Default to the non-anon version.
  2349. $messagetype = 'feedbackavailable';
  2350. // Message type needs 'anon' if "hidden grading" is enabled and the student
  2351. // doesn't have permission to see the grader.
  2352. if ($submission->hidegrader && !has_capability('mod/assign:showhiddengrader', $contextmodule, $user)) {
  2353. $messagetype = 'feedbackavailableanon';
  2354. // There's no point in having an "anonymous grader" if the notification email
  2355. // comes from them. Send the email from the noreply user instead.
  2356. $grader = core_user::get_noreply_user();
  2357. }
  2358. $eventtype = 'assign_notification';
  2359. $updatetime = $submission->lastmodified;
  2360. $modulename = get_string('modulename', 'assign');
  2361. $uniqueid = 0;
  2362. if ($submission->blindmarking && !$submission->revealidentities) {
  2363. if (empty($submission->recordid)) {
  2364. $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $grader->id);
  2365. } else {
  2366. $uniqueid = $submission->recordid;
  2367. }
  2368. }
  2369. $showusers = $submission->blindmarking && !$submission->revealidentities;
  2370. self::send_assignment_notification($grader,
  2371. $user,
  2372. $messagetype,
  2373. $eventtype,
  2374. $updatetime,
  2375. $cm,
  2376. $contextmodule,
  2377. $course,
  2378. $modulename,
  2379. $submission->name,
  2380. $showusers,
  2381. $uniqueid);
  2382. $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
  2383. if ($flags) {
  2384. $flags->mailed = 1;
  2385. $DB->update_record('assign_user_flags', $flags);
  2386. } else {
  2387. $flags = new stdClass();
  2388. $flags->userid = $user->id;
  2389. $flags->assignment = $submission->assignment;
  2390. $flags->mailed = 1;
  2391. $DB->insert_record('assign_user_flags', $flags);
  2392. }
  2393. mtrace('Done');
  2394. }
  2395. mtrace('Done processing ' . count($submissions) . ' assignment submissions');
  2396. cron_setup_user();
  2397. // Free up memory just to be sure.
  2398. unset($courses);
  2399. }
  2400. // Update calendar events to provide a description.
  2401. $sql = 'SELECT id
  2402. FROM {assign}
  2403. WHERE
  2404. allowsubmissionsfromdate >= :lastruntime AND
  2405. allowsubmissionsfromdate <= :timenow AND
  2406. alwaysshowdescription = 0';
  2407. $params = array('lastruntime' => $lastruntime, 'timenow' => $timenow);
  2408. $newlyavailable = $DB->get_records_sql($sql, $params);
  2409. foreach ($newlyavailable as $record) {
  2410. $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST);
  2411. $context = context_module::instance($cm->id);
  2412. $assignment = new assign($context, null, null);
  2413. $assignment->update_calendar($cm->id);
  2414. }
  2415. return true;
  2416. }
  2417. /**
  2418. * Mark in the database that this grade record should have an update notification sent by cron.
  2419. *
  2420. * @param stdClass $grade a grade record keyed on id
  2421. * @param bool $mailedoverride when true, flag notification to be sent again.
  2422. * @return bool true for success
  2423. */
  2424. public function notify_grade_modified($grade, $mailedoverride = false) {
  2425. global $DB;
  2426. $flags = $this->get_user_flags($grade->userid, true);
  2427. if ($flags->mailed != 1 || $mailedoverride) {
  2428. $flags->mailed = 0;
  2429. }
  2430. return $this->update_user_flags($flags);
  2431. }
  2432. /**
  2433. * Update user flags for this user in this assignment.
  2434. *
  2435. * @param stdClass $flags a flags record keyed on id
  2436. * @return bool true for success
  2437. */
  2438. public function update_user_flags($flags) {
  2439. global $DB;
  2440. if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) {
  2441. return false;
  2442. }
  2443. $result = $DB->update_record('assign_user_flags', $flags);
  2444. return $result;
  2445. }
  2446. /**
  2447. * Update a grade in the grade table for the assignment and in the gradebook.
  2448. *
  2449. * @param stdClass $grade a grade record keyed on id
  2450. * @param bool $reopenattempt If the attempt reopen method is manual, allow another attempt at this assignment.
  2451. * @return bool true for success
  2452. */
  2453. public function update_grade($grade, $reopenattempt = false) {
  2454. global $DB;
  2455. $grade->timemodified = time();
  2456. if (!empty($grade->workflowstate)) {
  2457. $validstates = $this->get_marking_workflow_states_for_current_user();
  2458. if (!array_key_exists($grade->workflowstate, $validstates)) {
  2459. return false;
  2460. }
  2461. }
  2462. if ($grade->grade && $grade->grade != -1) {
  2463. if ($this->get_instance()->grade > 0) {
  2464. if (!is_numeric($grade->grade)) {
  2465. return false;
  2466. } else if ($grade->grade > $this->get_instance()->grade) {
  2467. return false;
  2468. } else if ($grade->grade < 0) {
  2469. return false;
  2470. }
  2471. } else {
  2472. // This is a scale.
  2473. if ($scale = $DB->get_record('scale', array('id' => -($this->get_instance()->grade)))) {
  2474. $scaleoptions = make_menu_from_list($scale->scale);
  2475. if (!array_key_exists((int) $grade->grade, $scaleoptions)) {
  2476. return false;
  2477. }
  2478. }
  2479. }
  2480. }
  2481. if (empty($grade->attemptnumber)) {
  2482. // Set it to the default.
  2483. $grade->attemptnumber = 0;
  2484. }
  2485. $DB->update_record('assign_grades', $grade);
  2486. $submission = null;
  2487. if ($this->get_instance()->teamsubmission) {
  2488. if (isset($this->mostrecentteamsubmission)) {
  2489. $submission = $this->mostrecentteamsubmission;
  2490. } else {
  2491. $submission = $this->get_group_submission($grade->userid, 0, false);
  2492. }
  2493. } else {
  2494. $submission = $this->get_user_submission($grade->userid, false);
  2495. }
  2496. // Only push to gradebook if the update is for the most recent attempt.
  2497. if ($submission && $submission->attemptnumber != $grade->attemptnumber) {
  2498. return true;
  2499. }
  2500. if ($this->gradebook_item_update(null, $grade)) {
  2501. \mod_assign\event\submission_graded::create_from_grade($this, $grade)->trigger();
  2502. }
  2503. // If the conditions are met, allow another attempt.
  2504. if ($submission) {
  2505. $this->reopen_submission_if_required($grade->userid,
  2506. $submission,
  2507. $reopenattempt);
  2508. }
  2509. return true;
  2510. }
  2511. /**
  2512. * View the grant extension date page.
  2513. *
  2514. * Uses url parameters 'userid'
  2515. * or from parameter 'selectedusers'
  2516. *
  2517. * @param moodleform $mform - Used for validation of the submitted data
  2518. * @return string
  2519. */
  2520. protected function view_grant_extension($mform) {
  2521. global $CFG;
  2522. require_once($CFG->dirroot . '/mod/assign/extensionform.php');
  2523. $o = '';
  2524. $data = new stdClass();
  2525. $data->id = $this->get_course_module()->id;
  2526. $formparams = array(
  2527. 'instance' => $this->get_instance(),
  2528. 'assign' => $this
  2529. );
  2530. $users = optional_param('userid', 0, PARAM_INT);
  2531. if (!$users) {
  2532. $users = required_param('selectedusers', PARAM_SEQUENCE);
  2533. }
  2534. $userlist = explode(',', $users);
  2535. $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
  2536. $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
  2537. foreach ($userlist as $userid) {
  2538. // To validate extension date with users overrides.
  2539. $override = $this->override_exists($userid);
  2540. foreach ($keys as $key) {
  2541. if ($override->{$key}) {
  2542. if ($maxoverride[$key] < $override->{$key}) {
  2543. $maxoverride[$key] = $override->{$key};
  2544. }
  2545. } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
  2546. $maxoverride[$key] = $this->get_instance()->{$key};
  2547. }
  2548. }
  2549. }
  2550. foreach ($keys as $key) {
  2551. if ($maxoverride[$key]) {
  2552. $this->get_instance()->{$key} = $maxoverride[$key];
  2553. }
  2554. }
  2555. $formparams['userlist'] = $userlist;
  2556. $data->selectedusers = $users;
  2557. $data->userid = 0;
  2558. if (empty($mform)) {
  2559. $mform = new mod_assign_extension_form(null, $formparams);
  2560. }
  2561. $mform->set_data($data);
  2562. $header = new assign_header($this->get_instance(),
  2563. $this->get_context(),
  2564. $this->show_intro(),
  2565. $this->get_course_module()->id,
  2566. get_string('grantextension', 'assign'));
  2567. $o .= $this->get_renderer()->render($header);
  2568. $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform));
  2569. $o .= $this->view_footer();
  2570. return $o;
  2571. }
  2572. /**
  2573. * Get a list of the users in the same group as this user.
  2574. *
  2575. * @param int $groupid The id of the group whose members we want or 0 for the default group
  2576. * @param bool $onlyids Whether to retrieve only the user id's
  2577. * @param bool $excludesuspended Whether to exclude suspended users
  2578. * @return array The users (possibly id's only)
  2579. */
  2580. public function get_submission_group_members($groupid, $onlyids, $excludesuspended = false) {
  2581. $members = array();
  2582. if ($groupid != 0) {
  2583. $allusers = $this->list_participants($groupid, $onlyids);
  2584. foreach ($allusers as $user) {
  2585. if ($this->get_submission_group($user->id)) {
  2586. $members[] = $user;
  2587. }
  2588. }
  2589. } else {
  2590. $allusers = $this->list_participants(null, $onlyids);
  2591. foreach ($allusers as $user) {
  2592. if ($this->get_submission_group($user->id) == null) {
  2593. $members[] = $user;
  2594. }
  2595. }
  2596. }
  2597. // Exclude suspended users, if user can't see them.
  2598. if ($excludesuspended || !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
  2599. foreach ($members as $key => $member) {
  2600. if (!$this->is_active_user($member->id)) {
  2601. unset($members[$key]);
  2602. }
  2603. }
  2604. }
  2605. return $members;
  2606. }
  2607. /**
  2608. * Get a list of the users in the same group as this user that have not submitted the assignment.
  2609. *
  2610. * @param int $groupid The id of the group whose members we want or 0 for the default group
  2611. * @param bool $onlyids Whether to retrieve only the user id's
  2612. * @return array The users (possibly id's only)
  2613. */
  2614. public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) {
  2615. $instance = $this->get_instance();
  2616. if (!$instance->teamsubmission || !$instance->requireallteammemberssubmit) {
  2617. return array();
  2618. }
  2619. $members = $this->get_submission_group_members($groupid, $onlyids);
  2620. foreach ($members as $id => $member) {
  2621. $submission = $this->get_user_submission($member->id, false);
  2622. if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
  2623. unset($members[$id]);
  2624. } else {
  2625. if ($this->is_blind_marking()) {
  2626. $members[$id]->alias = get_string('hiddenuser', 'assign') .
  2627. $this->get_uniqueid_for_user($id);
  2628. }
  2629. }
  2630. }
  2631. return $members;
  2632. }
  2633. /**
  2634. * Load the group submission object for a particular user, optionally creating it if required.
  2635. *
  2636. * @param int $userid The id of the user whose submission we want
  2637. * @param int $groupid The id of the group for this user - may be 0 in which
  2638. * case it is determined from the userid.
  2639. * @param bool $create If set to true a new submission object will be created in the database
  2640. * with the status set to "new".
  2641. * @param int $attemptnumber - -1 means the latest attempt
  2642. * @return stdClass The submission
  2643. */
  2644. public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) {
  2645. global $DB;
  2646. if ($groupid == 0) {
  2647. $group = $this->get_submission_group($userid);
  2648. if ($group) {
  2649. $groupid = $group->id;
  2650. }
  2651. }
  2652. // Now get the group submission.
  2653. $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
  2654. if ($attemptnumber >= 0) {
  2655. $params['attemptnumber'] = $attemptnumber;
  2656. }
  2657. // Only return the row with the highest attemptnumber.
  2658. $submission = null;
  2659. $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
  2660. if ($submissions) {
  2661. $submission = reset($submissions);
  2662. }
  2663. if ($submission) {
  2664. return $submission;
  2665. }
  2666. if ($create) {
  2667. $submission = new stdClass();
  2668. $submission->assignment = $this->get_instance()->id;
  2669. $submission->userid = 0;
  2670. $submission->groupid = $groupid;
  2671. $submission->timecreated = time();
  2672. $submission->timemodified = $submission->timecreated;
  2673. if ($attemptnumber >= 0) {
  2674. $submission->attemptnumber = $attemptnumber;
  2675. } else {
  2676. $submission->attemptnumber = 0;
  2677. }
  2678. // Work out if this is the latest submission.
  2679. $submission->latest = 0;
  2680. $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
  2681. if ($attemptnumber == -1) {
  2682. // This is a new submission so it must be the latest.
  2683. $submission->latest = 1;
  2684. } else {
  2685. // We need to work this out.
  2686. $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
  2687. if ($result) {
  2688. $latestsubmission = reset($result);
  2689. }
  2690. if (!$latestsubmission || ($attemptnumber == $latestsubmission->attemptnumber)) {
  2691. $submission->latest = 1;
  2692. }
  2693. }
  2694. if ($submission->latest) {
  2695. // This is the case when we need to set latest to 0 for all the other attempts.
  2696. $DB->set_field('assign_submission', 'latest', 0, $params);
  2697. }
  2698. $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
  2699. $sid = $DB->insert_record('assign_submission', $submission);
  2700. return $DB->get_record('assign_submission', array('id' => $sid));
  2701. }
  2702. return false;
  2703. }
  2704. /**
  2705. * View a summary listing of all assignments in the current course.
  2706. *
  2707. * @return string
  2708. */
  2709. private function view_course_index() {
  2710. global $USER;
  2711. $o = '';
  2712. $course = $this->get_course();
  2713. $strplural = get_string('modulenameplural', 'assign');
  2714. if (!$cms = get_coursemodules_in_course('assign', $course->id, 'm.duedate')) {
  2715. $o .= $this->get_renderer()->notification(get_string('thereareno', 'moodle', $strplural));
  2716. $o .= $this->get_renderer()->continue_button(new moodle_url('/course/view.php', array('id' => $course->id)));
  2717. return $o;
  2718. }
  2719. $strsectionname = '';
  2720. $usesections = course_format_uses_sections($course->format);
  2721. $modinfo = get_fast_modinfo($course);
  2722. if ($usesections) {
  2723. $strsectionname = get_string('sectionname', 'format_'.$course->format);
  2724. $sections = $modinfo->get_section_info_all();
  2725. }
  2726. $courseindexsummary = new assign_course_index_summary($usesections, $strsectionname);
  2727. $timenow = time();
  2728. $currentsection = '';
  2729. foreach ($modinfo->instances['assign'] as $cm) {
  2730. if (!$cm->uservisible) {
  2731. continue;
  2732. }
  2733. $timedue = $cms[$cm->id]->duedate;
  2734. $sectionname = '';
  2735. if ($usesections && $cm->sectionnum) {
  2736. $sectionname = get_section_name($course, $sections[$cm->sectionnum]);
  2737. }
  2738. $submitted = '';
  2739. $context = context_module::instance($cm->id);
  2740. $assignment = new assign($context, $cm, $course);
  2741. // Apply overrides.
  2742. $assignment->update_effective_access($USER->id);
  2743. $timedue = $assignment->get_instance()->duedate;
  2744. if (has_capability('mod/assign:submit', $context) &&
  2745. !has_capability('moodle/site:config', $context)) {
  2746. $cangrade = false;
  2747. if ($assignment->get_instance()->teamsubmission) {
  2748. $usersubmission = $assignment->get_group_submission($USER->id, 0, false);
  2749. } else {
  2750. $usersubmission = $assignment->get_user_submission($USER->id, false);
  2751. }
  2752. if (!empty($usersubmission->status)) {
  2753. $submitted = get_string('submissionstatus_' . $usersubmission->status, 'assign');
  2754. } else {
  2755. $submitted = get_string('submissionstatus_', 'assign');
  2756. }
  2757. $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $cm->instance, $USER->id);
  2758. if (isset($gradinginfo->items[0]->grades[$USER->id]) &&
  2759. !$gradinginfo->items[0]->grades[$USER->id]->hidden ) {
  2760. $grade = $gradinginfo->items[0]->grades[$USER->id]->str_grade;
  2761. } else {
  2762. $grade = '-';
  2763. }
  2764. } else if (has_capability('mod/assign:grade', $context)) {
  2765. $submitted = $assignment->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED);
  2766. $grade = $assignment->count_submissions_need_grading();
  2767. $cangrade = true;
  2768. }
  2769. $courseindexsummary->add_assign_info($cm->id, $cm->get_formatted_name(),
  2770. $sectionname, $timedue, $submitted, $grade, $cangrade);
  2771. }
  2772. $o .= $this->get_renderer()->render($courseindexsummary);
  2773. $o .= $this->view_footer();
  2774. return $o;
  2775. }
  2776. /**
  2777. * View a page rendered by a plugin.
  2778. *
  2779. * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'.
  2780. *
  2781. * @return string
  2782. */
  2783. protected function view_plugin_page() {
  2784. global $USER;
  2785. $o = '';
  2786. $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
  2787. $plugintype = required_param('plugin', PARAM_PLUGIN);
  2788. $pluginaction = required_param('pluginaction', PARAM_ALPHA);
  2789. $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
  2790. if (!$plugin) {
  2791. print_error('invalidformdata', '');
  2792. return;
  2793. }
  2794. $o .= $plugin->view_page($pluginaction);
  2795. return $o;
  2796. }
  2797. /**
  2798. * This is used for team assignments to get the group for the specified user.
  2799. * If the user is a member of multiple or no groups this will return false
  2800. *
  2801. * @param int $userid The id of the user whose submission we want
  2802. * @return mixed The group or false
  2803. */
  2804. public function get_submission_group($userid) {
  2805. if (isset($this->usersubmissiongroups[$userid])) {
  2806. return $this->usersubmissiongroups[$userid];
  2807. }
  2808. $groups = $this->get_all_groups($userid);
  2809. if (count($groups) != 1) {
  2810. $return = false;
  2811. } else {
  2812. $return = array_pop($groups);
  2813. }
  2814. // Cache the user submission group.
  2815. $this->usersubmissiongroups[$userid] = $return;
  2816. return $return;
  2817. }
  2818. /**
  2819. * Gets all groups the user is a member of.
  2820. *
  2821. * @param int $userid Teh id of the user who's groups we are checking
  2822. * @return array The group objects
  2823. */
  2824. public function get_all_groups($userid) {
  2825. if (isset($this->usergroups[$userid])) {
  2826. return $this->usergroups[$userid];
  2827. }
  2828. $grouping = $this->get_instance()->teamsubmissiongroupingid;
  2829. $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping);
  2830. $this->usergroups[$userid] = $return;
  2831. return $return;
  2832. }
  2833. /**
  2834. * Display the submission that is used by a plugin.
  2835. *
  2836. * Uses url parameters 'sid', 'gid' and 'plugin'.
  2837. *
  2838. * @param string $pluginsubtype
  2839. * @return string
  2840. */
  2841. protected function view_plugin_content($pluginsubtype) {
  2842. $o = '';
  2843. $submissionid = optional_param('sid', 0, PARAM_INT);
  2844. $gradeid = optional_param('gid', 0, PARAM_INT);
  2845. $plugintype = required_param('plugin', PARAM_PLUGIN);
  2846. $item = null;
  2847. if ($pluginsubtype == 'assignsubmission') {
  2848. $plugin = $this->get_submission_plugin_by_type($plugintype);
  2849. if ($submissionid <= 0) {
  2850. throw new coding_exception('Submission id should not be 0');
  2851. }
  2852. $item = $this->get_submission($submissionid);
  2853. // Check permissions.
  2854. if (empty($item->userid)) {
  2855. // Group submission.
  2856. $this->require_view_group_submission($item->groupid);
  2857. } else {
  2858. $this->require_view_submission($item->userid);
  2859. }
  2860. $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
  2861. $this->get_context(),
  2862. $this->show_intro(),
  2863. $this->get_course_module()->id,
  2864. $plugin->get_name()));
  2865. $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin,
  2866. $item,
  2867. assign_submission_plugin_submission::FULL,
  2868. $this->get_course_module()->id,
  2869. $this->get_return_action(),
  2870. $this->get_return_params()));
  2871. // Trigger event for viewing a submission.
  2872. \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger();
  2873. } else {
  2874. $plugin = $this->get_feedback_plugin_by_type($plugintype);
  2875. if ($gradeid <= 0) {
  2876. throw new coding_exception('Grade id should not be 0');
  2877. }
  2878. $item = $this->get_grade($gradeid);
  2879. // Check permissions.
  2880. $this->require_view_submission($item->userid);
  2881. $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
  2882. $this->get_context(),
  2883. $this->show_intro(),
  2884. $this->get_course_module()->id,
  2885. $plugin->get_name()));
  2886. $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin,
  2887. $item,
  2888. assign_feedback_plugin_feedback::FULL,
  2889. $this->get_course_module()->id,
  2890. $this->get_return_action(),
  2891. $this->get_return_params()));
  2892. // Trigger event for viewing feedback.
  2893. \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger();
  2894. }
  2895. $o .= $this->view_return_links();
  2896. $o .= $this->view_footer();
  2897. return $o;
  2898. }
  2899. /**
  2900. * Rewrite plugin file urls so they resolve correctly in an exported zip.
  2901. *
  2902. * @param string $text - The replacement text
  2903. * @param stdClass $user - The user record
  2904. * @param assign_plugin $plugin - The assignment plugin
  2905. */
  2906. public function download_rewrite_pluginfile_urls($text, $user, $plugin) {
  2907. // The groupname prefix for the urls doesn't depend on the group mode of the assignment instance.
  2908. // Rather, it should be determined by checking the group submission settings of the instance,
  2909. // which is what download_submission() does when generating the file name prefixes.
  2910. $groupname = '';
  2911. if ($this->get_instance()->teamsubmission) {
  2912. $submissiongroup = $this->get_submission_group($user->id);
  2913. if ($submissiongroup) {
  2914. $groupname = $submissiongroup->name . '-';
  2915. } else {
  2916. $groupname = get_string('defaultteam', 'assign') . '-';
  2917. }
  2918. }
  2919. if ($this->is_blind_marking()) {
  2920. $prefix = $groupname . get_string('participant', 'assign');
  2921. $prefix = str_replace('_', ' ', $prefix);
  2922. $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
  2923. } else {
  2924. $prefix = $groupname . fullname($user);
  2925. $prefix = str_replace('_', ' ', $prefix);
  2926. $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
  2927. }
  2928. // Only prefix files if downloadasfolders user preference is NOT set.
  2929. if (!get_user_preferences('assign_downloadasfolders', 1)) {
  2930. $subtype = $plugin->get_subtype();
  2931. $type = $plugin->get_type();
  2932. $prefix = $prefix . $subtype . '_' . $type . '_';
  2933. } else {
  2934. $prefix = "";
  2935. }
  2936. $result = str_replace('@@PLUGINFILE@@/', $prefix, $text);
  2937. return $result;
  2938. }
  2939. /**
  2940. * Render the content in editor that is often used by plugin.
  2941. *
  2942. * @param string $filearea
  2943. * @param int $submissionid
  2944. * @param string $plugintype
  2945. * @param string $editor
  2946. * @param string $component
  2947. * @param bool $shortentext Whether to shorten the text content.
  2948. * @return string
  2949. */
  2950. public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component, $shortentext = false) {
  2951. global $CFG;
  2952. $result = '';
  2953. $plugin = $this->get_submission_plugin_by_type($plugintype);
  2954. $text = $plugin->get_editor_text($editor, $submissionid);
  2955. if ($shortentext) {
  2956. $text = shorten_text($text, 140);
  2957. }
  2958. $format = $plugin->get_editor_format($editor, $submissionid);
  2959. $finaltext = file_rewrite_pluginfile_urls($text,
  2960. 'pluginfile.php',
  2961. $this->get_context()->id,
  2962. $component,
  2963. $filearea,
  2964. $submissionid);
  2965. $params = array('overflowdiv' => true, 'context' => $this->get_context());
  2966. $result .= format_text($finaltext, $format, $params);
  2967. if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) {
  2968. require_once($CFG->libdir . '/portfoliolib.php');
  2969. $button = new portfolio_add_button();
  2970. $portfolioparams = array('cmid' => $this->get_course_module()->id,
  2971. 'sid' => $submissionid,
  2972. 'plugin' => $plugintype,
  2973. 'editor' => $editor,
  2974. 'area'=>$filearea);
  2975. $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign');
  2976. $fs = get_file_storage();
  2977. if ($files = $fs->get_area_files($this->context->id,
  2978. $component,
  2979. $filearea,
  2980. $submissionid,
  2981. 'timemodified',
  2982. false)) {
  2983. $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
  2984. } else {
  2985. $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
  2986. }
  2987. $result .= $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
  2988. }
  2989. return $result;
  2990. }
  2991. /**
  2992. * Display a continue page after grading.
  2993. *
  2994. * @param string $message - The message to display.
  2995. * @return string
  2996. */
  2997. protected function view_savegrading_result($message) {
  2998. $o = '';
  2999. $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
  3000. $this->get_context(),
  3001. $this->show_intro(),
  3002. $this->get_course_module()->id,
  3003. get_string('savegradingresult', 'assign')));
  3004. $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'),
  3005. $message,
  3006. $this->get_course_module()->id);
  3007. $o .= $this->get_renderer()->render($gradingresult);
  3008. $o .= $this->view_footer();
  3009. return $o;
  3010. }
  3011. /**
  3012. * Display a continue page after quickgrading.
  3013. *
  3014. * @param string $message - The message to display.
  3015. * @return string
  3016. */
  3017. protected function view_quickgrading_result($message) {
  3018. $o = '';
  3019. $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
  3020. $this->get_context(),
  3021. $this->show_intro(),
  3022. $this->get_course_module()->id,
  3023. get_string('quickgradingresult', 'assign')));
  3024. $gradingerror = in_array($message, $this->get_error_messages());
  3025. $lastpage = optional_param('lastpage', null, PARAM_INT);
  3026. $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'),
  3027. $message,
  3028. $this->get_course_module()->id,
  3029. $gradingerror,
  3030. $lastpage);
  3031. $o .= $this->get_renderer()->render($gradingresult);
  3032. $o .= $this->view_footer();
  3033. return $o;
  3034. }
  3035. /**
  3036. * Display the page footer.
  3037. *
  3038. * @return string
  3039. */
  3040. protected function view_footer() {
  3041. // When viewing the footer during PHPUNIT tests a set_state error is thrown.
  3042. if (!PHPUNIT_TEST) {
  3043. return $this->get_renderer()->render_footer();
  3044. }
  3045. return '';
  3046. }
  3047. /**
  3048. * Throw an error if the permissions to view this users' group submission are missing.
  3049. *
  3050. * @param int $groupid Group id.
  3051. * @throws required_capability_exception
  3052. */
  3053. public function require_view_group_submission($groupid) {
  3054. if (!$this->can_view_group_submission($groupid)) {
  3055. throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
  3056. }
  3057. }
  3058. /**
  3059. * Throw an error if the permissions to view this users submission are missing.
  3060. *
  3061. * @throws required_capability_exception
  3062. * @return none
  3063. */
  3064. public function require_view_submission($userid) {
  3065. if (!$this->can_view_submission($userid)) {
  3066. throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
  3067. }
  3068. }
  3069. /**
  3070. * Throw an error if the permissions to view grades in this assignment are missing.
  3071. *
  3072. * @throws required_capability_exception
  3073. * @return none
  3074. */
  3075. public function require_view_grades() {
  3076. if (!$this->can_view_grades()) {
  3077. throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
  3078. }
  3079. }
  3080. /**
  3081. * Does this user have view grade or grade permission for this assignment?
  3082. *
  3083. * @param mixed $groupid int|null when is set to a value, use this group instead calculating it
  3084. * @return bool
  3085. */
  3086. public function can_view_grades($groupid = null) {
  3087. // Permissions check.
  3088. if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
  3089. return false;
  3090. }
  3091. // Checks for the edge case when user belongs to no groups and groupmode is sep.
  3092. if ($this->get_course_module()->effectivegroupmode == SEPARATEGROUPS) {
  3093. if ($groupid === null) {
  3094. $groupid = groups_get_activity_allowed_groups($this->get_course_module());
  3095. }
  3096. $groupflag = has_capability('moodle/site:accessallgroups', $this->get_context());
  3097. $groupflag = $groupflag || !empty($groupid);
  3098. return (bool)$groupflag;
  3099. }
  3100. return true;
  3101. }
  3102. /**
  3103. * Does this user have grade permission for this assignment?
  3104. *
  3105. * @param int|stdClass $user The object or id of the user who will do the editing (default to current user).
  3106. * @return bool
  3107. */
  3108. public function can_grade($user = null) {
  3109. // Permissions check.
  3110. if (!has_capability('mod/assign:grade', $this->context, $user)) {
  3111. return false;
  3112. }
  3113. return true;
  3114. }
  3115. /**
  3116. * Download a zip file of all assignment submissions.
  3117. *
  3118. * @param array $userids Array of user ids to download assignment submissions in a zip file
  3119. * @return string - If an error occurs, this will contain the error page.
  3120. */
  3121. protected function download_submissions($userids = false) {
  3122. global $CFG, $DB;
  3123. // More efficient to load this here.
  3124. require_once($CFG->libdir.'/filelib.php');
  3125. // Increase the server timeout to handle the creation and sending of large zip files.
  3126. core_php_time_limit::raise();
  3127. $this->require_view_grades();
  3128. // Load all users with submit.
  3129. $students = get_enrolled_users($this->context, "mod/assign:submit", null, 'u.*', null, null, null,
  3130. $this->show_only_active_users());
  3131. // Build a list of files to zip.
  3132. $filesforzipping = array();
  3133. $fs = get_file_storage();
  3134. $groupmode = groups_get_activity_groupmode($this->get_course_module());
  3135. // All users.
  3136. $groupid = 0;
  3137. $groupname = '';
  3138. if ($groupmode) {
  3139. $groupid = groups_get_activity_group($this->get_course_module(), true);
  3140. if (!empty($groupid)) {
  3141. $groupname = groups_get_group_name($groupid) . '-';
  3142. }
  3143. }
  3144. // Construct the zip file name.
  3145. $filename = clean_filename($this->get_course()->shortname . '-' .
  3146. $this->get_instance()->name . '-' .
  3147. $groupname.$this->get_course_module()->id . '.zip');
  3148. // Get all the files for each student.
  3149. foreach ($students as $student) {
  3150. $userid = $student->id;
  3151. // Download all assigments submission or only selected users.
  3152. if ($userids and !in_array($userid, $userids)) {
  3153. continue;
  3154. }
  3155. if ((groups_is_member($groupid, $userid) or !$groupmode or !$groupid)) {
  3156. // Get the plugins to add their own files to the zip.
  3157. $submissiongroup = false;
  3158. $groupname = '';
  3159. if ($this->get_instance()->teamsubmission) {
  3160. $submission = $this->get_group_submission($userid, 0, false);
  3161. $submissiongroup = $this->get_submission_group($userid);
  3162. if ($submissiongroup) {
  3163. $groupname = $submissiongroup->name . '-';
  3164. } else {
  3165. $groupname = get_string('defaultteam', 'assign') . '-';
  3166. }
  3167. } else {
  3168. $submission = $this->get_user_submission($userid, false);
  3169. }
  3170. if ($this->is_blind_marking()) {
  3171. $prefix = str_replace('_', ' ', $groupname . get_string('participant', 'assign'));
  3172. $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
  3173. } else {
  3174. $fullname = fullname($student, has_capability('moodle/site:viewfullnames', $this->get_context()));
  3175. $prefix = str_replace('_', ' ', $groupname . $fullname);
  3176. $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
  3177. }
  3178. if ($submission) {
  3179. $downloadasfolders = get_user_preferences('assign_downloadasfolders', 1);
  3180. foreach ($this->submissionplugins as $plugin) {
  3181. if ($plugin->is_enabled() && $plugin->is_visible()) {
  3182. if ($downloadasfolders) {
  3183. // Create a folder for each user for each assignment plugin.
  3184. // This is the default behavior for version of Moodle >= 3.1.
  3185. $submission->exportfullpath = true;
  3186. $pluginfiles = $plugin->get_files($submission, $student);
  3187. foreach ($pluginfiles as $zipfilepath => $file) {
  3188. $subtype = $plugin->get_subtype();
  3189. $type = $plugin->get_type();
  3190. $zipfilename = basename($zipfilepath);
  3191. $prefixedfilename = clean_filename($prefix .
  3192. '_' .
  3193. $subtype .
  3194. '_' .
  3195. $type .
  3196. '_');
  3197. if ($type == 'file') {
  3198. $pathfilename = $prefixedfilename . $file->get_filepath() . $zipfilename;
  3199. } else if ($type == 'onlinetext') {
  3200. $pathfilename = $prefixedfilename . '/' . $zipfilename;
  3201. } else {
  3202. $pathfilename = $prefixedfilename . '/' . $zipfilename;
  3203. }
  3204. $pathfilename = clean_param($pathfilename, PARAM_PATH);
  3205. $filesforzipping[$pathfilename] = $file;
  3206. }
  3207. } else {
  3208. // Create a single folder for all users of all assignment plugins.
  3209. // This was the default behavior for version of Moodle < 3.1.
  3210. $submission->exportfullpath = false;
  3211. $pluginfiles = $plugin->get_files($submission, $student);
  3212. foreach ($pluginfiles as $zipfilename => $file) {
  3213. $subtype = $plugin->get_subtype();
  3214. $type = $plugin->get_type();
  3215. $prefixedfilename = clean_filename($prefix .
  3216. '_' .
  3217. $subtype .
  3218. '_' .
  3219. $type .
  3220. '_' .
  3221. $zipfilename);
  3222. $filesforzipping[$prefixedfilename] = $file;
  3223. }
  3224. }
  3225. }
  3226. }
  3227. }
  3228. }
  3229. }
  3230. $result = '';
  3231. if (count($filesforzipping) == 0) {
  3232. $header = new assign_header($this->get_instance(),
  3233. $this->get_context(),
  3234. '',
  3235. $this->get_course_module()->id,
  3236. get_string('downloadall', 'assign'));
  3237. $result .= $this->get_renderer()->render($header);
  3238. $result .= $this->get_renderer()->notification(get_string('nosubmission', 'assign'));
  3239. $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id,
  3240. 'action'=>'grading'));
  3241. $result .= $this->get_renderer()->continue_button($url);
  3242. $result .= $this->view_footer();
  3243. return $result;
  3244. }
  3245. // Log zip as downloaded.
  3246. \mod_assign\event\all_submissions_downloaded::create_from_assign($this)->trigger();
  3247. // Close the session.
  3248. \core\session\manager::write_close();
  3249. $zipwriter = \core_files\archive_writer::get_stream_writer($filename, \core_files\archive_writer::ZIP_WRITER);
  3250. // Stream the files into the zip.
  3251. foreach ($filesforzipping as $pathinzip => $file) {
  3252. if ($file instanceof \stored_file) {
  3253. // Most of cases are \stored_file.
  3254. $zipwriter->add_file_from_stored_file($pathinzip, $file);
  3255. } else if (is_array($file)) {
  3256. // Save $file as contents, from onlinetext subplugin.
  3257. $content = reset($file);
  3258. $zipwriter->add_file_from_string($pathinzip, $content);
  3259. }
  3260. }
  3261. // Finish the archive.
  3262. $zipwriter->finish();
  3263. exit();
  3264. }
  3265. /**
  3266. * Util function to add a message to the log.
  3267. *
  3268. * @deprecated since 2.7 - Use new events system instead.
  3269. * (see http://docs.moodle.org/dev/Migrating_logging_calls_in_plugins).
  3270. *
  3271. * @param string $action The current action
  3272. * @param string $info A detailed description of the change. But no more than 255 characters.
  3273. * @param string $url The url to the assign module instance.
  3274. * @param bool $return If true, returns the arguments, else adds to log. The purpose of this is to
  3275. * retrieve the arguments to use them with the new event system (Event 2).
  3276. * @return void|array
  3277. */
  3278. public function add_to_log($action = '', $info = '', $url='', $return = false) {
  3279. global $USER;
  3280. $fullurl = 'view.php?id=' . $this->get_course_module()->id;
  3281. if ($url != '') {
  3282. $fullurl .= '&' . $url;
  3283. }
  3284. $args = array(
  3285. $this->get_course()->id,
  3286. 'assign',
  3287. $action,
  3288. $fullurl,
  3289. $info,
  3290. $this->get_course_module()->id
  3291. );
  3292. if ($return) {
  3293. // We only need to call debugging when returning a value. This is because the call to
  3294. // call_user_func_array('add_to_log', $args) will trigger a debugging message of it's own.
  3295. debugging('The mod_assign add_to_log() function is now deprecated.', DEBUG_DEVELOPER);
  3296. return $args;
  3297. }
  3298. call_user_func_array('add_to_log', $args);
  3299. }
  3300. /**
  3301. * Lazy load the page renderer and expose the renderer to plugins.
  3302. *
  3303. * @return assign_renderer
  3304. */
  3305. public function get_renderer() {
  3306. global $PAGE;
  3307. if ($this->output) {
  3308. return $this->output;
  3309. }
  3310. $this->output = $PAGE->get_renderer('mod_assign', null, RENDERER_TARGET_GENERAL);
  3311. return $this->output;
  3312. }
  3313. /**
  3314. * Load the submission object for a particular user, optionally creating it if required.
  3315. *
  3316. * For team assignments there are 2 submissions - the student submission and the team submission
  3317. * All files are associated with the team submission but the status of the students contribution is
  3318. * recorded separately.
  3319. *
  3320. * @param int $userid The id of the user whose submission we want or 0 in which case USER->id is used
  3321. * @param bool $create If set to true a new submission object will be created in the database with the status set to "new".
  3322. * @param int $attemptnumber - -1 means the latest attempt
  3323. * @return stdClass The submission
  3324. */
  3325. public function get_user_submission($userid, $create, $attemptnumber=-1) {
  3326. global $DB, $USER;
  3327. if (!$userid) {
  3328. $userid = $USER->id;
  3329. }
  3330. // If the userid is not null then use userid.
  3331. $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
  3332. if ($attemptnumber >= 0) {
  3333. $params['attemptnumber'] = $attemptnumber;
  3334. }
  3335. // Only return the row with the highest attemptnumber.
  3336. $submission = null;
  3337. $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
  3338. if ($submissions) {
  3339. $submission = reset($submissions);
  3340. }
  3341. if ($submission) {
  3342. return $submission;
  3343. }
  3344. if ($create) {
  3345. $submission = new stdClass();
  3346. $submission->assignment = $this->get_instance()->id;
  3347. $submission->userid = $userid;
  3348. $submission->timecreated = time();
  3349. $submission->timemodified = $submission->timecreated;
  3350. $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
  3351. if ($attemptnumber >= 0) {
  3352. $submission->attemptnumber = $attemptnumber;
  3353. } else {
  3354. $submission->attemptnumber = 0;
  3355. }
  3356. // Work out if this is the latest submission.
  3357. $submission->latest = 0;
  3358. $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
  3359. if ($attemptnumber == -1) {
  3360. // This is a new submission so it must be the latest.
  3361. $submission->latest = 1;
  3362. } else {
  3363. // We need to work this out.
  3364. $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
  3365. $latestsubmission = null;
  3366. if ($result) {
  3367. $latestsubmission = reset($result);
  3368. }
  3369. if (empty($latestsubmission) || ($attemptnumber > $latestsubmission->attemptnumber)) {
  3370. $submission->latest = 1;
  3371. }
  3372. }
  3373. if ($submission->latest) {
  3374. // This is the case when we need to set latest to 0 for all the other attempts.
  3375. $DB->set_field('assign_submission', 'latest', 0, $params);
  3376. }
  3377. $sid = $DB->insert_record('assign_submission', $submission);
  3378. return $DB->get_record('assign_submission', array('id' => $sid));
  3379. }
  3380. return false;
  3381. }
  3382. /**
  3383. * Load the submission object from it's id.
  3384. *
  3385. * @param int $submissionid The id of the submission we want
  3386. * @return stdClass The submission
  3387. */
  3388. protected function get_submission($submissionid) {
  3389. global $DB;
  3390. $params = array('assignment'=>$this->get_instance()->id, 'id'=>$submissionid);
  3391. return $DB->get_record('assign_submission', $params, '*', MUST_EXIST);
  3392. }
  3393. /**
  3394. * This will retrieve a user flags object from the db optionally creating it if required.
  3395. * The user flags was split from the user_grades table in 2.5.
  3396. *
  3397. * @param int $userid The user we are getting the flags for.
  3398. * @param bool $create If true the flags record will be created if it does not exist
  3399. * @return stdClass The flags record
  3400. */
  3401. public function get_user_flags($userid, $create) {
  3402. global $DB, $USER;
  3403. // If the userid is not null then use userid.
  3404. if (!$userid) {
  3405. $userid = $USER->id;
  3406. }
  3407. $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
  3408. $flags = $DB->get_record('assign_user_flags', $params);
  3409. if ($flags) {
  3410. return $flags;
  3411. }
  3412. if ($create) {
  3413. $flags = new stdClass();
  3414. $flags->assignment = $this->get_instance()->id;
  3415. $flags->userid = $userid;
  3416. $flags->locked = 0;
  3417. $flags->extensionduedate = 0;
  3418. $flags->workflowstate = '';
  3419. $flags->allocatedmarker = 0;
  3420. // The mailed flag can be one of 3 values: 0 is unsent, 1 is sent and 2 is do not send yet.
  3421. // This is because students only want to be notified about certain types of update (grades and feedback).
  3422. $flags->mailed = 2;
  3423. $fid = $DB->insert_record('assign_user_flags', $flags);
  3424. $flags->id = $fid;
  3425. return $flags;
  3426. }
  3427. return false;
  3428. }
  3429. /**
  3430. * This will retrieve a grade object from the db, optionally creating it if required.
  3431. *
  3432. * @param int $userid The user we are grading
  3433. * @param bool $create If true the grade will be created if it does not exist
  3434. * @param int $attemptnumber The attempt number to retrieve the grade for. -1 means the latest submission.
  3435. * @return stdClass The grade record
  3436. */
  3437. public function get_user_grade($userid, $create, $attemptnumber=-1) {
  3438. global $DB, $USER;
  3439. // If the userid is not null then use userid.
  3440. if (!$userid) {
  3441. $userid = $USER->id;
  3442. }
  3443. $submission = null;
  3444. $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
  3445. if ($attemptnumber < 0 || $create) {
  3446. // Make sure this grade matches the latest submission attempt.
  3447. if ($this->get_instance()->teamsubmission) {
  3448. $submission = $this->get_group_submission($userid, 0, true, $attemptnumber);
  3449. } else {
  3450. $submission = $this->get_user_submission($userid, true, $attemptnumber);
  3451. }
  3452. if ($submission) {
  3453. $attemptnumber = $submission->attemptnumber;
  3454. }
  3455. }
  3456. if ($attemptnumber >= 0) {
  3457. $params['attemptnumber'] = $attemptnumber;
  3458. }
  3459. $grades = $DB->get_records('assign_grades', $params, 'attemptnumber DESC', '*', 0, 1);
  3460. if ($grades) {
  3461. return reset($grades);
  3462. }
  3463. if ($create) {
  3464. $grade = new stdClass();
  3465. $grade->assignment = $this->get_instance()->id;
  3466. $grade->userid = $userid;
  3467. $grade->timecreated = time();
  3468. // If we are "auto-creating" a grade - and there is a submission
  3469. // the new grade should not have a more recent timemodified value
  3470. // than the submission.
  3471. if ($submission) {
  3472. $grade->timemodified = $submission->timemodified;
  3473. } else {
  3474. $grade->timemodified = $grade->timecreated;
  3475. }
  3476. $grade->grade = -1;
  3477. // Do not set the grader id here as it would be the admin users which is incorrect.
  3478. $grade->grader = -1;
  3479. if ($attemptnumber >= 0) {
  3480. $grade->attemptnumber = $attemptnumber;
  3481. }
  3482. $gid = $DB->insert_record('assign_grades', $grade);
  3483. $grade->id = $gid;
  3484. return $grade;
  3485. }
  3486. return false;
  3487. }
  3488. /**
  3489. * This will retrieve a grade object from the db.
  3490. *
  3491. * @param int $gradeid The id of the grade
  3492. * @return stdClass The grade record
  3493. */
  3494. protected function get_grade($gradeid) {
  3495. global $DB;
  3496. $params = array('assignment'=>$this->get_instance()->id, 'id'=>$gradeid);
  3497. return $DB->get_record('assign_grades', $params, '*', MUST_EXIST);
  3498. }
  3499. /**
  3500. * Print the grading page for a single user submission.
  3501. *
  3502. * @param array $args Optional args array (better than pulling args from _GET and _POST)
  3503. * @return string
  3504. */
  3505. protected function view_single_grading_panel($args) {
  3506. global $DB, $CFG;
  3507. $o = '';
  3508. require_once($CFG->dirroot . '/mod/assign/gradeform.php');
  3509. // Need submit permission to submit an assignment.
  3510. require_capability('mod/assign:grade', $this->context);
  3511. // If userid is passed - we are only grading a single student.
  3512. $userid = $args['userid'];
  3513. $attemptnumber = $args['attemptnumber'];
  3514. $instance = $this->get_instance($userid);
  3515. // Apply overrides.
  3516. $this->update_effective_access($userid);
  3517. $rownum = 0;
  3518. $useridlist = array($userid);
  3519. $last = true;
  3520. // This variation on the url will link direct to this student, with no next/previous links.
  3521. // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
  3522. $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
  3523. $this->register_return_link('grade', $returnparams);
  3524. $user = $DB->get_record('user', array('id' => $userid));
  3525. $submission = $this->get_user_submission($userid, false, $attemptnumber);
  3526. $submissiongroup = null;
  3527. $teamsubmission = null;
  3528. $notsubmitted = array();
  3529. if ($instance->teamsubmission) {
  3530. $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
  3531. $submissiongroup = $this->get_submission_group($userid);
  3532. $groupid = 0;
  3533. if ($submissiongroup) {
  3534. $groupid = $submissiongroup->id;
  3535. }
  3536. $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
  3537. }
  3538. // Get the requested grade.
  3539. $grade = $this->get_user_grade($userid, false, $attemptnumber);
  3540. $flags = $this->get_user_flags($userid, false);
  3541. if ($this->can_view_submission($userid)) {
  3542. $submissionlocked = ($flags && $flags->locked);
  3543. $extensionduedate = null;
  3544. if ($flags) {
  3545. $extensionduedate = $flags->extensionduedate;
  3546. }
  3547. $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
  3548. $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
  3549. $usergroups = $this->get_all_groups($user->id);
  3550. $submissionstatus = new assign_submission_status_compact($instance->allowsubmissionsfromdate,
  3551. $instance->alwaysshowdescription,
  3552. $submission,
  3553. $instance->teamsubmission,
  3554. $teamsubmission,
  3555. $submissiongroup,
  3556. $notsubmitted,
  3557. $this->is_any_submission_plugin_enabled(),
  3558. $submissionlocked,
  3559. $this->is_graded($userid),
  3560. $instance->duedate,
  3561. $instance->cutoffdate,
  3562. $this->get_submission_plugins(),
  3563. $this->get_return_action(),
  3564. $this->get_return_params(),
  3565. $this->get_course_module()->id,
  3566. $this->get_course()->id,
  3567. assign_submission_status::GRADER_VIEW,
  3568. $showedit,
  3569. false,
  3570. $viewfullnames,
  3571. $extensionduedate,
  3572. $this->get_context(),
  3573. $this->is_blind_marking(),
  3574. '',
  3575. $instance->attemptreopenmethod,
  3576. $instance->maxattempts,
  3577. $this->get_grading_status($userid),
  3578. $instance->preventsubmissionnotingroup,
  3579. $usergroups);
  3580. $o .= $this->get_renderer()->render($submissionstatus);
  3581. }
  3582. if ($grade) {
  3583. $data = new stdClass();
  3584. if ($grade->grade !== null && $grade->grade >= 0) {
  3585. $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
  3586. }
  3587. } else {
  3588. $data = new stdClass();
  3589. $data->grade = '';
  3590. }
  3591. if (!empty($flags->workflowstate)) {
  3592. $data->workflowstate = $flags->workflowstate;
  3593. }
  3594. if (!empty($flags->allocatedmarker)) {
  3595. $data->allocatedmarker = $flags->allocatedmarker;
  3596. }
  3597. // Warning if required.
  3598. $allsubmissions = $this->get_all_submissions($userid);
  3599. if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
  3600. $params = array('attemptnumber' => $attemptnumber + 1,
  3601. 'totalattempts' => count($allsubmissions));
  3602. $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
  3603. $o .= $this->get_renderer()->notification($message);
  3604. }
  3605. $pagination = array('rownum' => $rownum,
  3606. 'useridlistid' => 0,
  3607. 'last' => $last,
  3608. 'userid' => $userid,
  3609. 'attemptnumber' => $attemptnumber,
  3610. 'gradingpanel' => true);
  3611. if (!empty($args['formdata'])) {
  3612. $data = (array) $data;
  3613. $data = (object) array_merge($data, $args['formdata']);
  3614. }
  3615. $formparams = array($this, $data, $pagination);
  3616. $mform = new mod_assign_grade_form(null,
  3617. $formparams,
  3618. 'post',
  3619. '',
  3620. array('class' => 'gradeform'));
  3621. if (!empty($args['formdata'])) {
  3622. // If we were passed form data - we want the form to check the data
  3623. // and show errors.
  3624. $mform->is_validated();
  3625. }
  3626. $o .= $this->get_renderer()->heading(get_string('gradenoun'), 3);
  3627. $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
  3628. if (count($allsubmissions) > 1) {
  3629. $allgrades = $this->get_all_grades($userid);
  3630. $history = new assign_attempt_history_chooser($allsubmissions,
  3631. $allgrades,
  3632. $this->get_course_module()->id,
  3633. $userid);
  3634. $o .= $this->get_renderer()->render($history);
  3635. }
  3636. \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
  3637. return $o;
  3638. }
  3639. /**
  3640. * Print the grading page for a single user submission.
  3641. *
  3642. * @param moodleform $mform
  3643. * @return string
  3644. */
  3645. protected function view_single_grade_page($mform) {
  3646. global $DB, $CFG, $SESSION;
  3647. $o = '';
  3648. $instance = $this->get_instance();
  3649. require_once($CFG->dirroot . '/mod/assign/gradeform.php');
  3650. // Need submit permission to submit an assignment.
  3651. require_capability('mod/assign:grade', $this->context);
  3652. $header = new assign_header($instance,
  3653. $this->get_context(),
  3654. false,
  3655. $this->get_course_module()->id,
  3656. get_string('grading', 'assign'));
  3657. $o .= $this->get_renderer()->render($header);
  3658. // If userid is passed - we are only grading a single student.
  3659. $rownum = optional_param('rownum', 0, PARAM_INT);
  3660. $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
  3661. $userid = optional_param('userid', 0, PARAM_INT);
  3662. $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
  3663. if (!$userid) {
  3664. $useridlist = $this->get_grading_userid_list(true, $useridlistid);
  3665. } else {
  3666. $rownum = 0;
  3667. $useridlistid = 0;
  3668. $useridlist = array($userid);
  3669. }
  3670. if ($rownum < 0 || $rownum > count($useridlist)) {
  3671. throw new coding_exception('Row is out of bounds for the current grading table: ' . $rownum);
  3672. }
  3673. $last = false;
  3674. $userid = $useridlist[$rownum];
  3675. if ($rownum == count($useridlist) - 1) {
  3676. $last = true;
  3677. }
  3678. // This variation on the url will link direct to this student, with no next/previous links.
  3679. // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
  3680. $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
  3681. $this->register_return_link('grade', $returnparams);
  3682. $user = $DB->get_record('user', array('id' => $userid));
  3683. if ($user) {
  3684. $this->update_effective_access($userid);
  3685. $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
  3686. $usersummary = new assign_user_summary($user,
  3687. $this->get_course()->id,
  3688. $viewfullnames,
  3689. $this->is_blind_marking(),
  3690. $this->get_uniqueid_for_user($user->id),
  3691. // TODO Does not support custom user profile fields (MDL-70456).
  3692. \core_user\fields::get_identity_fields($this->get_context(), false),
  3693. !$this->is_active_user($userid));
  3694. $o .= $this->get_renderer()->render($usersummary);
  3695. }
  3696. $submission = $this->get_user_submission($userid, false, $attemptnumber);
  3697. $submissiongroup = null;
  3698. $teamsubmission = null;
  3699. $notsubmitted = array();
  3700. if ($instance->teamsubmission) {
  3701. $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
  3702. $submissiongroup = $this->get_submission_group($userid);
  3703. $groupid = 0;
  3704. if ($submissiongroup) {
  3705. $groupid = $submissiongroup->id;
  3706. }
  3707. $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
  3708. }
  3709. // Get the requested grade.
  3710. $grade = $this->get_user_grade($userid, false, $attemptnumber);
  3711. $flags = $this->get_user_flags($userid, false);
  3712. if ($this->can_view_submission($userid)) {
  3713. $submissionlocked = ($flags && $flags->locked);
  3714. $extensionduedate = null;
  3715. if ($flags) {
  3716. $extensionduedate = $flags->extensionduedate;
  3717. }
  3718. $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
  3719. $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
  3720. $usergroups = $this->get_all_groups($user->id);
  3721. $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
  3722. $instance->alwaysshowdescription,
  3723. $submission,
  3724. $instance->teamsubmission,
  3725. $teamsubmission,
  3726. $submissiongroup,
  3727. $notsubmitted,
  3728. $this->is_any_submission_plugin_enabled(),
  3729. $submissionlocked,
  3730. $this->is_graded($userid),
  3731. $instance->duedate,
  3732. $instance->cutoffdate,
  3733. $this->get_submission_plugins(),
  3734. $this->get_return_action(),
  3735. $this->get_return_params(),
  3736. $this->get_course_module()->id,
  3737. $this->get_course()->id,
  3738. assign_submission_status::GRADER_VIEW,
  3739. $showedit,
  3740. false,
  3741. $viewfullnames,
  3742. $extensionduedate,
  3743. $this->get_context(),
  3744. $this->is_blind_marking(),
  3745. '',
  3746. $instance->attemptreopenmethod,
  3747. $instance->maxattempts,
  3748. $this->get_grading_status($userid),
  3749. $instance->preventsubmissionnotingroup,
  3750. $usergroups);
  3751. $o .= $this->get_renderer()->render($submissionstatus);
  3752. }
  3753. if ($grade) {
  3754. $data = new stdClass();
  3755. if ($grade->grade !== null && $grade->grade >= 0) {
  3756. $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
  3757. }
  3758. } else {
  3759. $data = new stdClass();
  3760. $data->grade = '';
  3761. }
  3762. if (!empty($flags->workflowstate)) {
  3763. $data->workflowstate = $flags->workflowstate;
  3764. }
  3765. if (!empty($flags->allocatedmarker)) {
  3766. $data->allocatedmarker = $flags->allocatedmarker;
  3767. }
  3768. // Warning if required.
  3769. $allsubmissions = $this->get_all_submissions($userid);
  3770. if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
  3771. $params = array('attemptnumber'=>$attemptnumber + 1,
  3772. 'totalattempts'=>count($allsubmissions));
  3773. $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
  3774. $o .= $this->get_renderer()->notification($message);
  3775. }
  3776. // Now show the grading form.
  3777. if (!$mform) {
  3778. $pagination = array('rownum' => $rownum,
  3779. 'useridlistid' => $useridlistid,
  3780. 'last' => $last,
  3781. 'userid' => $userid,
  3782. 'attemptnumber' => $attemptnumber);
  3783. $formparams = array($this, $data, $pagination);
  3784. $mform = new mod_assign_grade_form(null,
  3785. $formparams,
  3786. 'post',
  3787. '',
  3788. array('class'=>'gradeform'));
  3789. }
  3790. $o .= $this->get_renderer()->heading(get_string('gradenoun'), 3);
  3791. $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
  3792. if (count($allsubmissions) > 1 && $attemptnumber == -1) {
  3793. $allgrades = $this->get_all_grades($userid);
  3794. $history = new assign_attempt_history($allsubmissions,
  3795. $allgrades,
  3796. $this->get_submission_plugins(),
  3797. $this->get_feedback_plugins(),
  3798. $this->get_course_module()->id,
  3799. $this->get_return_action(),
  3800. $this->get_return_params(),
  3801. true,
  3802. $useridlistid,
  3803. $rownum);
  3804. $o .= $this->get_renderer()->render($history);
  3805. }
  3806. \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
  3807. $o .= $this->view_footer();
  3808. return $o;
  3809. }
  3810. /**
  3811. * Show a confirmation page to make sure they want to remove submission data.
  3812. *
  3813. * @return string
  3814. */
  3815. protected function view_remove_submission_confirm() {
  3816. global $USER, $DB;
  3817. $userid = optional_param('userid', $USER->id, PARAM_INT);
  3818. if (!$this->can_edit_submission($userid, $USER->id)) {
  3819. print_error('nopermission');
  3820. }
  3821. $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
  3822. $o = '';
  3823. $header = new assign_header($this->get_instance(),
  3824. $this->get_context(),
  3825. false,
  3826. $this->get_course_module()->id);
  3827. $o .= $this->get_renderer()->render($header);
  3828. $urlparams = array('id' => $this->get_course_module()->id,
  3829. 'action' => 'removesubmission',
  3830. 'userid' => $userid,
  3831. 'sesskey' => sesskey());
  3832. $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
  3833. $urlparams = array('id' => $this->get_course_module()->id,
  3834. 'action' => 'view');
  3835. $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
  3836. if ($userid == $USER->id) {
  3837. $confirmstr = get_string('removesubmissionconfirm', 'assign');
  3838. } else {
  3839. $name = $this->fullname($user);
  3840. $confirmstr = get_string('removesubmissionconfirmforstudent', 'assign', $name);
  3841. }
  3842. $o .= $this->get_renderer()->confirm($confirmstr,
  3843. $confirmurl,
  3844. $cancelurl);
  3845. $o .= $this->view_footer();
  3846. \mod_assign\event\remove_submission_form_viewed::create_from_user($this, $user)->trigger();
  3847. return $o;
  3848. }
  3849. /**
  3850. * Show a confirmation page to make sure they want to release student identities.
  3851. *
  3852. * @return string
  3853. */
  3854. protected function view_reveal_identities_confirm() {
  3855. require_capability('mod/assign:revealidentities', $this->get_context());
  3856. $o = '';
  3857. $header = new assign_header($this->get_instance(),
  3858. $this->get_context(),
  3859. false,
  3860. $this->get_course_module()->id);
  3861. $o .= $this->get_renderer()->render($header);
  3862. $urlparams = array('id'=>$this->get_course_module()->id,
  3863. 'action'=>'revealidentitiesconfirm',
  3864. 'sesskey'=>sesskey());
  3865. $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
  3866. $urlparams = array('id'=>$this->get_course_module()->id,
  3867. 'action'=>'grading');
  3868. $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
  3869. $o .= $this->get_renderer()->confirm(get_string('revealidentitiesconfirm', 'assign'),
  3870. $confirmurl,
  3871. $cancelurl);
  3872. $o .= $this->view_footer();
  3873. \mod_assign\event\reveal_identities_confirmation_page_viewed::create_from_assign($this)->trigger();
  3874. return $o;
  3875. }
  3876. /**
  3877. * View a link to go back to the previous page. Uses url parameters returnaction and returnparams.
  3878. *
  3879. * @return string
  3880. */
  3881. protected function view_return_links() {
  3882. $returnaction = optional_param('returnaction', '', PARAM_ALPHA);
  3883. $returnparams = optional_param('returnparams', '', PARAM_TEXT);
  3884. $params = array();
  3885. $returnparams = str_replace('&amp;', '&', $returnparams);
  3886. parse_str($returnparams, $params);
  3887. $newparams = array('id' => $this->get_course_module()->id, 'action' => $returnaction);
  3888. $params = array_merge($newparams, $params);
  3889. $url = new moodle_url('/mod/assign/view.php', $params);
  3890. return $this->get_renderer()->single_button($url, get_string('back'), 'get');
  3891. }
  3892. /**
  3893. * View the grading table of all submissions for this assignment.
  3894. *
  3895. * @return string
  3896. */
  3897. protected function view_grading_table() {
  3898. global $USER, $CFG, $SESSION;
  3899. // Include grading options form.
  3900. require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
  3901. require_once($CFG->dirroot . '/mod/assign/quickgradingform.php');
  3902. require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
  3903. $o = '';
  3904. $cmid = $this->get_course_module()->id;
  3905. $links = array();
  3906. if (has_capability('gradereport/grader:view', $this->get_course_context()) &&
  3907. has_capability('moodle/grade:viewall', $this->get_course_context())) {
  3908. $gradebookurl = '/grade/report/grader/index.php?id=' . $this->get_course()->id;
  3909. $links[$gradebookurl] = get_string('viewgradebook', 'assign');
  3910. }
  3911. if ($this->is_any_submission_plugin_enabled() && $this->count_submissions()) {
  3912. $downloadurl = '/mod/assign/view.php?id=' . $cmid . '&action=downloadall';
  3913. $links[$downloadurl] = get_string('downloadall', 'assign');
  3914. }
  3915. if ($this->is_blind_marking() &&
  3916. has_capability('mod/assign:revealidentities', $this->get_context())) {
  3917. $revealidentitiesurl = '/mod/assign/view.php?id=' . $cmid . '&action=revealidentities';
  3918. $links[$revealidentitiesurl] = get_string('revealidentities', 'assign');
  3919. }
  3920. foreach ($this->get_feedback_plugins() as $plugin) {
  3921. if ($plugin->is_enabled() && $plugin->is_visible()) {
  3922. foreach ($plugin->get_grading_actions() as $action => $description) {
  3923. $url = '/mod/assign/view.php' .
  3924. '?id=' . $cmid .
  3925. '&plugin=' . $plugin->get_type() .
  3926. '&pluginsubtype=assignfeedback' .
  3927. '&action=viewpluginpage&pluginaction=' . $action;
  3928. $links[$url] = $description;
  3929. }
  3930. }
  3931. }
  3932. // Sort links alphabetically based on the link description.
  3933. core_collator::asort($links);
  3934. $gradingactions = new url_select($links);
  3935. $gradingactions->set_label(get_string('choosegradingaction', 'assign'));
  3936. $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
  3937. $perpage = $this->get_assign_perpage();
  3938. $filter = get_user_preferences('assign_filter', '');
  3939. $markerfilter = get_user_preferences('assign_markerfilter', '');
  3940. $workflowfilter = get_user_preferences('assign_workflowfilter', '');
  3941. $controller = $gradingmanager->get_active_controller();
  3942. $showquickgrading = empty($controller) && $this->can_grade();
  3943. $quickgrading = get_user_preferences('assign_quickgrading', false);
  3944. $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
  3945. $downloadasfolders = get_user_preferences('assign_downloadasfolders', 1);
  3946. $markingallocation = $this->get_instance()->markingworkflow &&
  3947. $this->get_instance()->markingallocation &&
  3948. has_capability('mod/assign:manageallocations', $this->context);
  3949. // Get markers to use in drop lists.
  3950. $markingallocationoptions = array();
  3951. if ($markingallocation) {
  3952. list($sort, $params) = users_order_by_sql('u');
  3953. // Only enrolled users could be assigned as potential markers.
  3954. $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
  3955. $markingallocationoptions[''] = get_string('filternone', 'assign');
  3956. $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
  3957. $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
  3958. foreach ($markers as $marker) {
  3959. $markingallocationoptions[$marker->id] = fullname($marker, $viewfullnames);
  3960. }
  3961. }
  3962. $markingworkflow = $this->get_instance()->markingworkflow;
  3963. // Get marking states to show in form.
  3964. $markingworkflowoptions = $this->get_marking_workflow_filters();
  3965. // Print options for changing the filter and changing the number of results per page.
  3966. $gradingoptionsformparams = array('cm'=>$cmid,
  3967. 'contextid'=>$this->context->id,
  3968. 'userid'=>$USER->id,
  3969. 'submissionsenabled'=>$this->is_any_submission_plugin_enabled(),
  3970. 'showquickgrading'=>$showquickgrading,
  3971. 'quickgrading'=>$quickgrading,
  3972. 'markingworkflowopt'=>$markingworkflowoptions,
  3973. 'markingallocationopt'=>$markingallocationoptions,
  3974. 'showonlyactiveenrolopt'=>$showonlyactiveenrolopt,
  3975. 'showonlyactiveenrol' => $this->show_only_active_users(),
  3976. 'downloadasfolders' => $downloadasfolders);
  3977. $classoptions = array('class'=>'gradingoptionsform');
  3978. $gradingoptionsform = new mod_assign_grading_options_form(null,
  3979. $gradingoptionsformparams,
  3980. 'post',
  3981. '',
  3982. $classoptions);
  3983. $batchformparams = array('cm'=>$cmid,
  3984. 'submissiondrafts'=>$this->get_instance()->submissiondrafts,
  3985. 'duedate'=>$this->get_instance()->duedate,
  3986. 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
  3987. 'feedbackplugins'=>$this->get_feedback_plugins(),
  3988. 'context'=>$this->get_context(),
  3989. 'markingworkflow'=>$markingworkflow,
  3990. 'markingallocation'=>$markingallocation);
  3991. $classoptions = array('class'=>'gradingbatchoperationsform');
  3992. $gradingbatchoperationsform = new mod_assign_grading_batch_operations_form(null,
  3993. $batchformparams,
  3994. 'post',
  3995. '',
  3996. $classoptions);
  3997. $gradingoptionsdata = new stdClass();
  3998. $gradingoptionsdata->perpage = $perpage;
  3999. $gradingoptionsdata->filter = $filter;
  4000. $gradingoptionsdata->markerfilter = $markerfilter;
  4001. $gradingoptionsdata->workflowfilter = $workflowfilter;
  4002. $gradingoptionsform->set_data($gradingoptionsdata);
  4003. $actionformtext = $this->get_renderer()->render($gradingactions);
  4004. $currenturl = new moodle_url('/mod/assign/view.php', ['id' => $this->get_course_module()->id, 'action' => 'grading']);
  4005. $header = new assign_header($this->get_instance(),
  4006. $this->get_context(),
  4007. false,
  4008. $this->get_course_module()->id,
  4009. get_string('grading', 'assign'),
  4010. $actionformtext,
  4011. '',
  4012. $currenturl);
  4013. $o .= $this->get_renderer()->render($header);
  4014. $o .= groups_print_activity_menu($this->get_course_module(), $currenturl, true);
  4015. // Plagiarism update status apearring in the grading book.
  4016. if (!empty($CFG->enableplagiarism)) {
  4017. require_once($CFG->libdir . '/plagiarismlib.php');
  4018. $o .= plagiarism_update_status($this->get_course(), $this->get_course_module());
  4019. }
  4020. if ($this->is_blind_marking() && has_capability('mod/assign:viewblinddetails', $this->get_context())) {
  4021. $o .= $this->get_renderer()->notification(get_string('blindmarkingenabledwarning', 'assign'), 'notifymessage');
  4022. }
  4023. // Load and print the table of submissions.
  4024. if ($showquickgrading && $quickgrading) {
  4025. $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, true);
  4026. $table = $this->get_renderer()->render($gradingtable);
  4027. $page = optional_param('page', null, PARAM_INT);
  4028. $quickformparams = array('cm'=>$this->get_course_module()->id,
  4029. 'gradingtable'=>$table,
  4030. 'sendstudentnotifications' => $this->get_instance()->sendstudentnotifications,
  4031. 'page' => $page);
  4032. $quickgradingform = new mod_assign_quick_grading_form(null, $quickformparams);
  4033. $o .= $this->get_renderer()->render(new assign_form('quickgradingform', $quickgradingform));
  4034. } else {
  4035. $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, false);
  4036. $o .= $this->get_renderer()->render($gradingtable);
  4037. }
  4038. if ($this->can_grade()) {
  4039. // We need to store the order of uses in the table as the person may wish to grade them.
  4040. // This is done based on the row number of the user.
  4041. $useridlist = $gradingtable->get_column_data('userid');
  4042. $SESSION->mod_assign_useridlist[$this->get_useridlist_key()] = $useridlist;
  4043. }
  4044. $currentgroup = groups_get_activity_group($this->get_course_module(), true);
  4045. $users = array_keys($this->list_participants($currentgroup, true));
  4046. if (count($users) != 0 && $this->can_grade()) {
  4047. // If no enrolled user in a course then don't display the batch operations feature.
  4048. $assignform = new assign_form('gradingbatchoperationsform', $gradingbatchoperationsform);
  4049. $o .= $this->get_renderer()->render($assignform);
  4050. }
  4051. $assignform = new assign_form('gradingoptionsform',
  4052. $gradingoptionsform,
  4053. 'M.mod_assign.init_grading_options');
  4054. $o .= $this->get_renderer()->render($assignform);
  4055. return $o;
  4056. }
  4057. /**
  4058. * View entire grader app.
  4059. *
  4060. * @return string
  4061. */
  4062. protected function view_grader() {
  4063. global $USER, $PAGE;
  4064. $o = '';
  4065. // Need submit permission to submit an assignment.
  4066. $this->require_view_grades();
  4067. $PAGE->set_pagelayout('embedded');
  4068. $courseshortname = $this->get_context()->get_course_context()->get_context_name(false, true);
  4069. $args = [
  4070. 'contextname' => $this->get_context()->get_context_name(false, true),
  4071. 'subpage' => get_string('grading', 'assign')
  4072. ];
  4073. $title = get_string('subpagetitle', 'assign', $args);
  4074. $title = $courseshortname . ': ' . $title;
  4075. $PAGE->set_title($title);
  4076. $o .= $this->get_renderer()->header();
  4077. $userid = optional_param('userid', 0, PARAM_INT);
  4078. $blindid = optional_param('blindid', 0, PARAM_INT);
  4079. if (!$userid && $blindid) {
  4080. $userid = $this->get_user_id_for_uniqueid($blindid);
  4081. }
  4082. $currentgroup = groups_get_activity_group($this->get_course_module(), true);
  4083. $framegrader = new grading_app($userid, $currentgroup, $this);
  4084. $this->update_effective_access($userid);
  4085. $o .= $this->get_renderer()->render($framegrader);
  4086. $o .= $this->view_footer();
  4087. \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
  4088. return $o;
  4089. }
  4090. /**
  4091. * View entire grading page.
  4092. *
  4093. * @return string
  4094. */
  4095. protected function view_grading_page() {
  4096. global $CFG;
  4097. $o = '';
  4098. // Need submit permission to submit an assignment.
  4099. $this->require_view_grades();
  4100. require_once($CFG->dirroot . '/mod/assign/gradeform.php');
  4101. $this->add_grade_notices();
  4102. // Only load this if it is.
  4103. $o .= $this->view_grading_table();
  4104. $o .= $this->view_footer();
  4105. \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
  4106. return $o;
  4107. }
  4108. /**
  4109. * Capture the output of the plagiarism plugins disclosures and return it as a string.
  4110. *
  4111. * @return string
  4112. */
  4113. protected function plagiarism_print_disclosure() {
  4114. global $CFG;
  4115. $o = '';
  4116. if (!empty($CFG->enableplagiarism)) {
  4117. require_once($CFG->libdir . '/plagiarismlib.php');
  4118. $o .= plagiarism_print_disclosure($this->get_course_module()->id);
  4119. }
  4120. return $o;
  4121. }
  4122. /**
  4123. * Message for students when assignment submissions have been closed.
  4124. *
  4125. * @param string $title The page title
  4126. * @param array $notices The array of notices to show.
  4127. * @return string
  4128. */
  4129. protected function view_notices($title, $notices) {
  4130. global $CFG;
  4131. $o = '';
  4132. $header = new assign_header($this->get_instance(),
  4133. $this->get_context(),
  4134. $this->show_intro(),
  4135. $this->get_course_module()->id,
  4136. $title);
  4137. $o .= $this->get_renderer()->render($header);
  4138. foreach ($notices as $notice) {
  4139. $o .= $this->get_renderer()->notification($notice);
  4140. }
  4141. $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id, 'action'=>'view'));
  4142. $o .= $this->get_renderer()->continue_button($url);
  4143. $o .= $this->view_footer();
  4144. return $o;
  4145. }
  4146. /**
  4147. * Get the name for a user - hiding their real name if blind marking is on.
  4148. *
  4149. * @param stdClass $user The user record as required by fullname()
  4150. * @return string The name.
  4151. */
  4152. public function fullname($user) {
  4153. if ($this->is_blind_marking()) {
  4154. $hasviewblind = has_capability('mod/assign:viewblinddetails', $this->get_context());
  4155. if (empty($user->recordid)) {
  4156. $uniqueid = $this->get_uniqueid_for_user($user->id);
  4157. } else {
  4158. $uniqueid = $user->recordid;
  4159. }
  4160. if ($hasviewblind) {
  4161. return get_string('participant', 'assign') . ' ' . $uniqueid . ' (' .
  4162. fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())) . ')';
  4163. } else {
  4164. return get_string('participant', 'assign') . ' ' . $uniqueid;
  4165. }
  4166. } else {
  4167. return fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context()));
  4168. }
  4169. }
  4170. /**
  4171. * View edit submissions page.
  4172. *
  4173. * @param moodleform $mform
  4174. * @param array $notices A list of notices to display at the top of the
  4175. * edit submission form (e.g. from plugins).
  4176. * @return string The page output.
  4177. */
  4178. protected function view_edit_submission_page($mform, $notices) {
  4179. global $CFG, $USER, $DB;
  4180. $o = '';
  4181. require_once($CFG->dirroot . '/mod/assign/submission_form.php');
  4182. // Need submit permission to submit an assignment.
  4183. $userid = optional_param('userid', $USER->id, PARAM_INT);
  4184. $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
  4185. // This variation on the url will link direct to this student.
  4186. // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
  4187. $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
  4188. $this->register_return_link('editsubmission', $returnparams);
  4189. if ($userid == $USER->id) {
  4190. if (!$this->can_edit_submission($userid, $USER->id)) {
  4191. print_error('nopermission');
  4192. }
  4193. // User is editing their own submission.
  4194. require_capability('mod/assign:submit', $this->context);
  4195. $title = get_string('editsubmission', 'assign');
  4196. } else {
  4197. // User is editing another user's submission.
  4198. if (!$this->can_edit_submission($userid, $USER->id)) {
  4199. print_error('nopermission');
  4200. }
  4201. $name = $this->fullname($user);
  4202. $title = get_string('editsubmissionother', 'assign', $name);
  4203. }
  4204. if (!$this->submissions_open($userid)) {
  4205. $message = array(get_string('submissionsclosed', 'assign'));
  4206. return $this->view_notices($title, $message);
  4207. }
  4208. $postfix = '';
  4209. if ($this->has_visible_attachments()) {
  4210. $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
  4211. }
  4212. $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
  4213. $this->get_context(),
  4214. $this->show_intro(),
  4215. $this->get_course_module()->id,
  4216. $title, '', $postfix));
  4217. // Show plagiarism disclosure for any user submitter.
  4218. $o .= $this->plagiarism_print_disclosure();
  4219. $data = new stdClass();
  4220. $data->userid = $userid;
  4221. if (!$mform) {
  4222. $mform = new mod_assign_submission_form(null, array($this, $data));
  4223. }
  4224. foreach ($notices as $notice) {
  4225. $o .= $this->get_renderer()->notification($notice);
  4226. }
  4227. $o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
  4228. $o .= $this->view_footer();
  4229. \mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger();
  4230. return $o;
  4231. }
  4232. /**
  4233. * See if this assignment has a grade yet.
  4234. *
  4235. * @param int $userid
  4236. * @return bool
  4237. */
  4238. protected function is_graded($userid) {
  4239. $grade = $this->get_user_grade($userid, false);
  4240. if ($grade) {
  4241. return ($grade->grade !== null && $grade->grade >= 0);
  4242. }
  4243. return false;
  4244. }
  4245. /**
  4246. * Perform an access check to see if the current $USER can edit this group submission.
  4247. *
  4248. * @param int $groupid
  4249. * @return bool
  4250. */
  4251. public function can_edit_group_submission($groupid) {
  4252. global $USER;
  4253. $members = $this->get_submission_group_members($groupid, true);
  4254. foreach ($members as $member) {
  4255. // If we can edit any members submission, we can edit the submission for the group.
  4256. if ($this->can_edit_submission($member->id)) {
  4257. return true;
  4258. }
  4259. }
  4260. return false;
  4261. }
  4262. /**
  4263. * Perform an access check to see if the current $USER can view this group submission.
  4264. *
  4265. * @param int $groupid
  4266. * @return bool
  4267. */
  4268. public function can_view_group_submission($groupid) {
  4269. global $USER;
  4270. $members = $this->get_submission_group_members($groupid, true);
  4271. foreach ($members as $member) {
  4272. // If we can view any members submission, we can view the submission for the group.
  4273. if ($this->can_view_submission($member->id)) {
  4274. return true;
  4275. }
  4276. }
  4277. return false;
  4278. }
  4279. /**
  4280. * Perform an access check to see if the current $USER can view this users submission.
  4281. *
  4282. * @param int $userid
  4283. * @return bool
  4284. */
  4285. public function can_view_submission($userid) {
  4286. global $USER;
  4287. if (!$this->is_active_user($userid) && !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
  4288. return false;
  4289. }
  4290. if (!is_enrolled($this->get_course_context(), $userid)) {
  4291. return false;
  4292. }
  4293. if (has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
  4294. return true;
  4295. }
  4296. if ($userid == $USER->id) {
  4297. return true;
  4298. }
  4299. return false;
  4300. }
  4301. /**
  4302. * Allows the plugin to show a batch grading operation page.
  4303. *
  4304. * @param moodleform $mform
  4305. * @return none
  4306. */
  4307. protected function view_plugin_grading_batch_operation($mform) {
  4308. require_capability('mod/assign:grade', $this->context);
  4309. $prefix = 'plugingradingbatchoperation_';
  4310. if ($data = $mform->get_data()) {
  4311. $tail = substr($data->operation, strlen($prefix));
  4312. list($plugintype, $action) = explode('_', $tail, 2);
  4313. $plugin = $this->get_feedback_plugin_by_type($plugintype);
  4314. if ($plugin) {
  4315. $users = $data->selectedusers;
  4316. $userlist = explode(',', $users);
  4317. echo $plugin->grading_batch_operation($action, $userlist);
  4318. return;
  4319. }
  4320. }
  4321. print_error('invalidformdata', '');
  4322. }
  4323. /**
  4324. * Ask the user to confirm they want to perform this batch operation
  4325. *
  4326. * @param moodleform $mform Set to a grading batch operations form
  4327. * @return string - the page to view after processing these actions
  4328. */
  4329. protected function process_grading_batch_operation(& $mform) {
  4330. global $CFG;
  4331. require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
  4332. require_sesskey();
  4333. $markingallocation = $this->get_instance()->markingworkflow &&
  4334. $this->get_instance()->markingallocation &&
  4335. has_capability('mod/assign:manageallocations', $this->context);
  4336. $batchformparams = array('cm'=>$this->get_course_module()->id,
  4337. 'submissiondrafts'=>$this->get_instance()->submissiondrafts,
  4338. 'duedate'=>$this->get_instance()->duedate,
  4339. 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
  4340. 'feedbackplugins'=>$this->get_feedback_plugins(),
  4341. 'context'=>$this->get_context(),
  4342. 'markingworkflow'=>$this->get_instance()->markingworkflow,
  4343. 'markingallocation'=>$markingallocation);
  4344. $formclasses = array('class'=>'gradingbatchoperationsform');
  4345. $mform = new mod_assign_grading_batch_operations_form(null,
  4346. $batchformparams,
  4347. 'post',
  4348. '',
  4349. $formclasses);
  4350. if ($data = $mform->get_data()) {
  4351. // Get the list of users.
  4352. $users = $data->selectedusers;
  4353. $userlist = explode(',', $users);
  4354. $prefix = 'plugingradingbatchoperation_';
  4355. if ($data->operation == 'grantextension') {
  4356. // Reset the form so the grant extension page will create the extension form.
  4357. $mform = null;
  4358. return 'grantextension';
  4359. } else if ($data->operation == 'setmarkingworkflowstate') {
  4360. return 'viewbatchsetmarkingworkflowstate';
  4361. } else if ($data->operation == 'setmarkingallocation') {
  4362. return 'viewbatchmarkingallocation';
  4363. } else if (strpos($data->operation, $prefix) === 0) {
  4364. $tail = substr($data->operation, strlen($prefix));
  4365. list($plugintype, $action) = explode('_', $tail, 2);
  4366. $plugin = $this->get_feedback_plugin_by_type($plugintype);
  4367. if ($plugin) {
  4368. return 'plugingradingbatchoperation';
  4369. }
  4370. }
  4371. if ($data->operation == 'downloadselected') {
  4372. $this->download_submissions($userlist);
  4373. } else {
  4374. foreach ($userlist as $userid) {
  4375. if ($data->operation == 'lock') {
  4376. $this->process_lock_submission($userid);
  4377. } else if ($data->operation == 'unlock') {
  4378. $this->process_unlock_submission($userid);
  4379. } else if ($data->operation == 'reverttodraft') {
  4380. $this->process_revert_to_draft($userid);
  4381. } else if ($data->operation == 'removesubmission') {
  4382. $this->process_remove_submission($userid);
  4383. } else if ($data->operation == 'addattempt') {
  4384. if (!$this->get_instance()->teamsubmission) {
  4385. $this->process_add_attempt($userid);
  4386. }
  4387. }
  4388. }
  4389. }
  4390. if ($this->get_instance()->teamsubmission && $data->operation == 'addattempt') {
  4391. // This needs to be handled separately so that each team submission is only re-opened one time.
  4392. $this->process_add_attempt_group($userlist);
  4393. }
  4394. }
  4395. return 'grading';
  4396. }
  4397. /**
  4398. * Shows a form that allows the workflow state for selected submissions to be changed.
  4399. *
  4400. * @param moodleform $mform Set to a grading batch operations form
  4401. * @return string - the page to view after processing these actions
  4402. */
  4403. protected function view_batch_set_workflow_state($mform) {
  4404. global $CFG, $DB;
  4405. require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
  4406. $o = '';
  4407. $submitteddata = $mform->get_data();
  4408. $users = $submitteddata->selectedusers;
  4409. $userlist = explode(',', $users);
  4410. $formdata = array('id' => $this->get_course_module()->id,
  4411. 'selectedusers' => $users);
  4412. $usershtml = '';
  4413. $usercount = 0;
  4414. // TODO Does not support custom user profile fields (MDL-70456).
  4415. $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false);
  4416. $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
  4417. foreach ($userlist as $userid) {
  4418. if ($usercount >= 5) {
  4419. $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
  4420. break;
  4421. }
  4422. $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
  4423. $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
  4424. $this->get_course()->id,
  4425. $viewfullnames,
  4426. $this->is_blind_marking(),
  4427. $this->get_uniqueid_for_user($user->id),
  4428. $extrauserfields,
  4429. !$this->is_active_user($userid)));
  4430. $usercount += 1;
  4431. }
  4432. $formparams = array(
  4433. 'userscount' => count($userlist),
  4434. 'usershtml' => $usershtml,
  4435. 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
  4436. );
  4437. $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
  4438. $mform->set_data($formdata); // Initialises the hidden elements.
  4439. $header = new assign_header($this->get_instance(),
  4440. $this->get_context(),
  4441. $this->show_intro(),
  4442. $this->get_course_module()->id,
  4443. get_string('setmarkingworkflowstate', 'assign'));
  4444. $o .= $this->get_renderer()->render($header);
  4445. $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
  4446. $o .= $this->view_footer();
  4447. \mod_assign\event\batch_set_workflow_state_viewed::create_from_assign($this)->trigger();
  4448. return $o;
  4449. }
  4450. /**
  4451. * Shows a form that allows the allocated marker for selected submissions to be changed.
  4452. *
  4453. * @param moodleform $mform Set to a grading batch operations form
  4454. * @return string - the page to view after processing these actions
  4455. */
  4456. public function view_batch_markingallocation($mform) {
  4457. global $CFG, $DB;
  4458. require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
  4459. $o = '';
  4460. $submitteddata = $mform->get_data();
  4461. $users = $submitteddata->selectedusers;
  4462. $userlist = explode(',', $users);
  4463. $formdata = array('id' => $this->get_course_module()->id,
  4464. 'selectedusers' => $users);
  4465. $usershtml = '';
  4466. $usercount = 0;
  4467. // TODO Does not support custom user profile fields (MDL-70456).
  4468. $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false);
  4469. $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
  4470. foreach ($userlist as $userid) {
  4471. if ($usercount >= 5) {
  4472. $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
  4473. break;
  4474. }
  4475. $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
  4476. $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
  4477. $this->get_course()->id,
  4478. $viewfullnames,
  4479. $this->is_blind_marking(),
  4480. $this->get_uniqueid_for_user($user->id),
  4481. $extrauserfields,
  4482. !$this->is_active_user($userid)));
  4483. $usercount += 1;
  4484. }
  4485. $formparams = array(
  4486. 'userscount' => count($userlist),
  4487. 'usershtml' => $usershtml,
  4488. );
  4489. list($sort, $params) = users_order_by_sql('u');
  4490. // Only enrolled users could be assigned as potential markers.
  4491. $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
  4492. $markerlist = array();
  4493. foreach ($markers as $marker) {
  4494. $markerlist[$marker->id] = fullname($marker);
  4495. }
  4496. $formparams['markers'] = $markerlist;
  4497. $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
  4498. $mform->set_data($formdata); // Initialises the hidden elements.
  4499. $header = new assign_header($this->get_instance(),
  4500. $this->get_context(),
  4501. $this->show_intro(),
  4502. $this->get_course_module()->id,
  4503. get_string('setmarkingallocation', 'assign'));
  4504. $o .= $this->get_renderer()->render($header);
  4505. $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
  4506. $o .= $this->view_footer();
  4507. \mod_assign\event\batch_set_marker_allocation_viewed::create_from_assign($this)->trigger();
  4508. return $o;
  4509. }
  4510. /**
  4511. * Ask the user to confirm they want to submit their work for grading.
  4512. *
  4513. * @param moodleform $mform - null unless form validation has failed
  4514. * @return string
  4515. */
  4516. protected function check_submit_for_grading($mform) {
  4517. global $USER, $CFG;
  4518. require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
  4519. // Check that all of the submission plugins are ready for this submission.
  4520. // Also check whether there is something to be submitted as well against atleast one.
  4521. $notifications = array();
  4522. $submission = $this->get_user_submission($USER->id, false);
  4523. if ($this->get_instance()->teamsubmission) {
  4524. $submission = $this->get_group_submission($USER->id, 0, false);
  4525. }
  4526. $plugins = $this->get_submission_plugins();
  4527. $hassubmission = false;
  4528. foreach ($plugins as $plugin) {
  4529. if ($plugin->is_enabled() && $plugin->is_visible()) {
  4530. $check = $plugin->precheck_submission($submission);
  4531. if ($check !== true) {
  4532. $notifications[] = $check;
  4533. }
  4534. if (is_object($submission) && !$plugin->is_empty($submission)) {
  4535. $hassubmission = true;
  4536. }
  4537. }
  4538. }
  4539. // If there are no submissions and no existing notifications to be displayed the stop.
  4540. if (!$hassubmission && !$notifications) {
  4541. $notifications[] = get_string('addsubmission_help', 'assign');
  4542. }
  4543. $data = new stdClass();
  4544. $adminconfig = $this->get_admin_config();
  4545. $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
  4546. $submissionstatement = '';
  4547. if ($requiresubmissionstatement) {
  4548. $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
  4549. }
  4550. // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
  4551. // that the submission statement checkbox will be displayed.
  4552. if (empty($submissionstatement)) {
  4553. $requiresubmissionstatement = false;
  4554. }
  4555. if ($mform == null) {
  4556. $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
  4557. $submissionstatement,
  4558. $this->get_course_module()->id,
  4559. $data));
  4560. }
  4561. $o = '';
  4562. $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
  4563. $this->get_context(),
  4564. $this->show_intro(),
  4565. $this->get_course_module()->id,
  4566. get_string('confirmsubmissionheading', 'assign')));
  4567. $submitforgradingpage = new assign_submit_for_grading_page($notifications,
  4568. $this->get_course_module()->id,
  4569. $mform);
  4570. $o .= $this->get_renderer()->render($submitforgradingpage);
  4571. $o .= $this->view_footer();
  4572. \mod_assign\event\submission_confirmation_form_viewed::create_from_assign($this)->trigger();
  4573. return $o;
  4574. }
  4575. /**
  4576. * Creates an assign_submission_status renderable.
  4577. *
  4578. * @param stdClass $user the user to get the report for
  4579. * @param bool $showlinks return plain text or links to the profile
  4580. * @return assign_submission_status renderable object
  4581. */
  4582. public function get_assign_submission_status_renderable($user, $showlinks) {
  4583. global $PAGE;
  4584. $instance = $this->get_instance();
  4585. $flags = $this->get_user_flags($user->id, false);
  4586. $submission = $this->get_user_submission($user->id, false);
  4587. $teamsubmission = null;
  4588. $submissiongroup = null;
  4589. $notsubmitted = array();
  4590. if ($instance->teamsubmission) {
  4591. $teamsubmission = $this->get_group_submission($user->id, 0, false);
  4592. $submissiongroup = $this->get_submission_group($user->id);
  4593. $groupid = 0;
  4594. if ($submissiongroup) {
  4595. $groupid = $submissiongroup->id;
  4596. }
  4597. $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
  4598. }
  4599. $showedit = $showlinks &&
  4600. ($this->is_any_submission_plugin_enabled()) &&
  4601. $this->can_edit_submission($user->id);
  4602. $submissionlocked = ($flags && $flags->locked);
  4603. // Grading criteria preview.
  4604. $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
  4605. $gradingcontrollerpreview = '';
  4606. if ($gradingmethod = $gradingmanager->get_active_method()) {
  4607. $controller = $gradingmanager->get_controller($gradingmethod);
  4608. if ($controller->is_form_defined()) {
  4609. $gradingcontrollerpreview = $controller->render_preview($PAGE);
  4610. }
  4611. }
  4612. $showsubmit = ($showlinks && $this->submissions_open($user->id));
  4613. $showsubmit = ($showsubmit && $this->show_submit_button($submission, $teamsubmission, $user->id));
  4614. $extensionduedate = null;
  4615. if ($flags) {
  4616. $extensionduedate = $flags->extensionduedate;
  4617. }
  4618. $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
  4619. $gradingstatus = $this->get_grading_status($user->id);
  4620. $usergroups = $this->get_all_groups($user->id);
  4621. $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
  4622. $instance->alwaysshowdescription,
  4623. $submission,
  4624. $instance->teamsubmission,
  4625. $teamsubmission,
  4626. $submissiongroup,
  4627. $notsubmitted,
  4628. $this->is_any_submission_plugin_enabled(),
  4629. $submissionlocked,
  4630. $this->is_graded($user->id),
  4631. $instance->duedate,
  4632. $instance->cutoffdate,
  4633. $this->get_submission_plugins(),
  4634. $this->get_return_action(),
  4635. $this->get_return_params(),
  4636. $this->get_course_module()->id,
  4637. $this->get_course()->id,
  4638. assign_submission_status::STUDENT_VIEW,
  4639. $showedit,
  4640. $showsubmit,
  4641. $viewfullnames,
  4642. $extensionduedate,
  4643. $this->get_context(),
  4644. $this->is_blind_marking(),
  4645. $gradingcontrollerpreview,
  4646. $instance->attemptreopenmethod,
  4647. $instance->maxattempts,
  4648. $gradingstatus,
  4649. $instance->preventsubmissionnotingroup,
  4650. $usergroups);
  4651. return $submissionstatus;
  4652. }
  4653. /**
  4654. * Creates an assign_feedback_status renderable.
  4655. *
  4656. * @param stdClass $user the user to get the report for
  4657. * @return assign_feedback_status renderable object
  4658. */
  4659. public function get_assign_feedback_status_renderable($user) {
  4660. global $CFG, $DB, $PAGE;
  4661. require_once($CFG->libdir.'/gradelib.php');
  4662. require_once($CFG->dirroot.'/grade/grading/lib.php');
  4663. $instance = $this->get_instance();
  4664. $grade = $this->get_user_grade($user->id, false);
  4665. $gradingstatus = $this->get_grading_status($user->id);
  4666. $gradinginfo = grade_get_grades($this->get_course()->id,
  4667. 'mod',
  4668. 'assign',
  4669. $instance->id,
  4670. $user->id);
  4671. $gradingitem = null;
  4672. $gradebookgrade = null;
  4673. if (isset($gradinginfo->items[0])) {
  4674. $gradingitem = $gradinginfo->items[0];
  4675. $gradebookgrade = $gradingitem->grades[$user->id];
  4676. }
  4677. // Check to see if all feedback plugins are empty.
  4678. $emptyplugins = true;
  4679. if ($grade) {
  4680. foreach ($this->get_feedback_plugins() as $plugin) {
  4681. if ($plugin->is_visible() && $plugin->is_enabled()) {
  4682. if (!$plugin->is_empty($grade)) {
  4683. $emptyplugins = false;
  4684. }
  4685. }
  4686. }
  4687. }
  4688. if ($this->get_instance()->markingworkflow && $gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
  4689. $emptyplugins = true; // Don't show feedback plugins until released either.
  4690. }
  4691. $cangrade = has_capability('mod/assign:grade', $this->get_context());
  4692. $hasgrade = $this->get_instance()->grade != GRADE_TYPE_NONE &&
  4693. !is_null($gradebookgrade) && !is_null($gradebookgrade->grade);
  4694. $gradevisible = $cangrade || $this->get_instance()->grade == GRADE_TYPE_NONE ||
  4695. (!is_null($gradebookgrade) && !$gradebookgrade->hidden);
  4696. // If there is a visible grade, show the summary.
  4697. if (($hasgrade || !$emptyplugins) && $gradevisible) {
  4698. $gradefordisplay = null;
  4699. $gradeddate = null;
  4700. $grader = null;
  4701. $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
  4702. if ($hasgrade) {
  4703. if ($controller = $gradingmanager->get_active_controller()) {
  4704. $menu = make_grades_menu($this->get_instance()->grade);
  4705. $controller->set_grade_range($menu, $this->get_instance()->grade > 0);
  4706. $gradefordisplay = $controller->render_grade($PAGE,
  4707. $grade->id,
  4708. $gradingitem,
  4709. $gradebookgrade->str_long_grade,
  4710. $cangrade);
  4711. } else {
  4712. $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
  4713. }
  4714. $gradeddate = $gradebookgrade->dategraded;
  4715. // Only display the grader if it is in the right state.
  4716. if (in_array($gradingstatus, [ASSIGN_GRADING_STATUS_GRADED, ASSIGN_MARKING_WORKFLOW_STATE_RELEASED])){
  4717. if (isset($grade->grader) && $grade->grader > 0) {
  4718. $grader = $DB->get_record('user', array('id' => $grade->grader));
  4719. } else if (isset($gradebookgrade->usermodified)
  4720. && $gradebookgrade->usermodified > 0
  4721. && has_capability('mod/assign:grade', $this->get_context(), $gradebookgrade->usermodified)) {
  4722. // Grader not provided. Check that usermodified is a user who can grade.
  4723. // Case 1: When an assignment is reopened an empty assign_grade is created so the feedback
  4724. // plugin can know which attempt it's referring to. In this case, usermodifed is a student.
  4725. // Case 2: When an assignment's grade is overrided via the gradebook, usermodified is a grader
  4726. $grader = $DB->get_record('user', array('id' => $gradebookgrade->usermodified));
  4727. }
  4728. }
  4729. }
  4730. $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
  4731. if ($grade) {
  4732. \mod_assign\event\feedback_viewed::create_from_grade($this, $grade)->trigger();
  4733. }
  4734. $feedbackstatus = new assign_feedback_status($gradefordisplay,
  4735. $gradeddate,
  4736. $grader,
  4737. $this->get_feedback_plugins(),
  4738. $grade,
  4739. $this->get_course_module()->id,
  4740. $this->get_return_action(),
  4741. $this->get_return_params(),
  4742. $viewfullnames);
  4743. // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability.
  4744. $showgradername = (
  4745. has_capability('mod/assign:showhiddengrader', $this->context) or
  4746. !$this->is_hidden_grader()
  4747. );
  4748. if (!$showgradername) {
  4749. $feedbackstatus->grader = false;
  4750. }
  4751. return $feedbackstatus;
  4752. }
  4753. return;
  4754. }
  4755. /**
  4756. * Creates an assign_attempt_history renderable.
  4757. *
  4758. * @param stdClass $user the user to get the report for
  4759. * @return assign_attempt_history renderable object
  4760. */
  4761. public function get_assign_attempt_history_renderable($user) {
  4762. $allsubmissions = $this->get_all_submissions($user->id);
  4763. $allgrades = $this->get_all_grades($user->id);
  4764. $history = new assign_attempt_history($allsubmissions,
  4765. $allgrades,
  4766. $this->get_submission_plugins(),
  4767. $this->get_feedback_plugins(),
  4768. $this->get_course_module()->id,
  4769. $this->get_return_action(),
  4770. $this->get_return_params(),
  4771. false,
  4772. 0,
  4773. 0);
  4774. return $history;
  4775. }
  4776. /**
  4777. * Print 2 tables of information with no action links -
  4778. * the submission summary and the grading summary.
  4779. *
  4780. * @param stdClass $user the user to print the report for
  4781. * @param bool $showlinks - Return plain text or links to the profile
  4782. * @return string - the html summary
  4783. */
  4784. public function view_student_summary($user, $showlinks) {
  4785. $o = '';
  4786. if ($this->can_view_submission($user->id)) {
  4787. if (has_capability('mod/assign:viewownsubmissionsummary', $this->get_context(), $user, false)) {
  4788. // The user can view the submission summary.
  4789. $submissionstatus = $this->get_assign_submission_status_renderable($user, $showlinks);
  4790. $o .= $this->get_renderer()->render($submissionstatus);
  4791. }
  4792. // If there is a visible grade, show the feedback.
  4793. $feedbackstatus = $this->get_assign_feedback_status_renderable($user);
  4794. if ($feedbackstatus) {
  4795. $o .= $this->get_renderer()->render($feedbackstatus);
  4796. }
  4797. // If there is more than one submission, show the history.
  4798. $history = $this->get_assign_attempt_history_renderable($user);
  4799. if (count($history->submissions) > 1) {
  4800. $o .= $this->get_renderer()->render($history);
  4801. }
  4802. }
  4803. return $o;
  4804. }
  4805. /**
  4806. * Returns true if the submit subsission button should be shown to the user.
  4807. *
  4808. * @param stdClass $submission The users own submission record.
  4809. * @param stdClass $teamsubmission The users team submission record if there is one
  4810. * @param int $userid The user
  4811. * @return bool
  4812. */
  4813. protected function show_submit_button($submission = null, $teamsubmission = null, $userid = null) {
  4814. if (!has_capability('mod/assign:submit', $this->get_context(), $userid, false)) {
  4815. // The user does not have the capability to submit.
  4816. return false;
  4817. }
  4818. if ($teamsubmission) {
  4819. if ($teamsubmission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
  4820. // The assignment submission has been completed.
  4821. return false;
  4822. } else if ($this->submission_empty($teamsubmission)) {
  4823. // There is nothing to submit yet.
  4824. return false;
  4825. } else if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
  4826. // The user has already clicked the submit button on the team submission.
  4827. return false;
  4828. } else if (
  4829. !empty($this->get_instance()->preventsubmissionnotingroup)
  4830. && $this->get_submission_group($userid) == false
  4831. ) {
  4832. return false;
  4833. }
  4834. } else if ($submission) {
  4835. if ($submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
  4836. // The assignment submission has been completed.
  4837. return false;
  4838. } else if ($this->submission_empty($submission)) {
  4839. // There is nothing to submit.
  4840. return false;
  4841. }
  4842. } else {
  4843. // We've not got a valid submission or team submission.
  4844. return false;
  4845. }
  4846. // Last check is that this instance allows drafts.
  4847. return $this->get_instance()->submissiondrafts;
  4848. }
  4849. /**
  4850. * Get the grades for all previous attempts.
  4851. * For each grade - the grader is a full user record,
  4852. * and gradefordisplay is added (rendered from grading manager).
  4853. *
  4854. * @param int $userid If not set, $USER->id will be used.
  4855. * @return array $grades All grade records for this user.
  4856. */
  4857. protected function get_all_grades($userid) {
  4858. global $DB, $USER, $PAGE;
  4859. // If the userid is not null then use userid.
  4860. if (!$userid) {
  4861. $userid = $USER->id;
  4862. }
  4863. $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
  4864. $grades = $DB->get_records('assign_grades', $params, 'attemptnumber ASC');
  4865. $gradercache = array();
  4866. $cangrade = has_capability('mod/assign:grade', $this->get_context());
  4867. // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability.
  4868. $showgradername = (
  4869. has_capability('mod/assign:showhiddengrader', $this->context, $userid) or
  4870. !$this->is_hidden_grader()
  4871. );
  4872. // Need gradingitem and gradingmanager.
  4873. $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
  4874. $controller = $gradingmanager->get_active_controller();
  4875. $gradinginfo = grade_get_grades($this->get_course()->id,
  4876. 'mod',
  4877. 'assign',
  4878. $this->get_instance()->id,
  4879. $userid);
  4880. $gradingitem = null;
  4881. if (isset($gradinginfo->items[0])) {
  4882. $gradingitem = $gradinginfo->items[0];
  4883. }
  4884. foreach ($grades as $grade) {
  4885. // First lookup the grader info.
  4886. if (!$showgradername) {
  4887. $grade->grader = null;
  4888. } else if (isset($gradercache[$grade->grader])) {
  4889. $grade->grader = $gradercache[$grade->grader];
  4890. } else if ($grade->grader > 0) {
  4891. // Not in cache - need to load the grader record.
  4892. $grade->grader = $DB->get_record('user', array('id'=>$grade->grader));
  4893. if ($grade->grader) {
  4894. $gradercache[$grade->grader->id] = $grade->grader;
  4895. }
  4896. }
  4897. // Now get the gradefordisplay.
  4898. if ($controller) {
  4899. $controller->set_grade_range(make_grades_menu($this->get_instance()->grade), $this->get_instance()->grade > 0);
  4900. $grade->gradefordisplay = $controller->render_grade($PAGE,
  4901. $grade->id,
  4902. $gradingitem,
  4903. $grade->grade,
  4904. $cangrade);
  4905. } else {
  4906. $grade->gradefordisplay = $this->display_grade($grade->grade, false);
  4907. }
  4908. }
  4909. return $grades;
  4910. }
  4911. /**
  4912. * Get the submissions for all previous attempts.
  4913. *
  4914. * @param int $userid If not set, $USER->id will be used.
  4915. * @return array $submissions All submission records for this user (or group).
  4916. */
  4917. public function get_all_submissions($userid) {
  4918. global $DB, $USER;
  4919. // If the userid is not null then use userid.
  4920. if (!$userid) {
  4921. $userid = $USER->id;
  4922. }
  4923. $params = array();
  4924. if ($this->get_instance()->teamsubmission) {
  4925. $groupid = 0;
  4926. $group = $this->get_submission_group($userid);
  4927. if ($group) {
  4928. $groupid = $group->id;
  4929. }
  4930. // Params to get the group submissions.
  4931. $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
  4932. } else {
  4933. // Params to get the user submissions.
  4934. $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
  4935. }
  4936. // Return the submissions ordered by attempt.
  4937. $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber ASC');
  4938. return $submissions;
  4939. }
  4940. /**
  4941. * Creates an assign_grading_summary renderable.
  4942. *
  4943. * @param mixed $activitygroup int|null the group for calculating the grading summary (if null the function will determine it)
  4944. * @return assign_grading_summary renderable object
  4945. */
  4946. public function get_assign_grading_summary_renderable($activitygroup = null) {
  4947. $instance = $this->get_default_instance(); // Grading summary requires the raw dates, regardless of relativedates mode.
  4948. $cm = $this->get_course_module();
  4949. $course = $this->get_course();
  4950. $draft = ASSIGN_SUBMISSION_STATUS_DRAFT;
  4951. $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
  4952. $isvisible = $cm->visible;
  4953. if ($activitygroup === null) {
  4954. $activitygroup = groups_get_activity_group($cm);
  4955. }
  4956. if ($instance->teamsubmission) {
  4957. $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_NO;
  4958. $defaultteammembers = $this->get_submission_group_members(0, true);
  4959. if (count($defaultteammembers) > 0) {
  4960. if ($instance->preventsubmissionnotingroup) {
  4961. $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_REQUIRED;
  4962. } else {
  4963. $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_OPTIONAL;
  4964. }
  4965. }
  4966. $summary = new assign_grading_summary(
  4967. $this->count_teams($activitygroup),
  4968. $instance->submissiondrafts,
  4969. $this->count_submissions_with_status($draft, $activitygroup),
  4970. $this->is_any_submission_plugin_enabled(),
  4971. $this->count_submissions_with_status($submitted, $activitygroup),
  4972. $instance->cutoffdate,
  4973. $this->get_duedate($activitygroup),
  4974. $this->get_course_module()->id,
  4975. $this->count_submissions_need_grading($activitygroup),
  4976. $instance->teamsubmission,
  4977. $warnofungroupedusers,
  4978. $course->relativedatesmode,
  4979. $course->startdate,
  4980. $this->can_grade(),
  4981. $isvisible
  4982. );
  4983. } else {
  4984. // The active group has already been updated in groups_print_activity_menu().
  4985. $countparticipants = $this->count_participants($activitygroup);
  4986. $summary = new assign_grading_summary(
  4987. $countparticipants,
  4988. $instance->submissiondrafts,
  4989. $this->count_submissions_with_status($draft, $activitygroup),
  4990. $this->is_any_submission_plugin_enabled(),
  4991. $this->count_submissions_with_status($submitted, $activitygroup),
  4992. $instance->cutoffdate,
  4993. $this->get_duedate($activitygroup),
  4994. $this->get_course_module()->id,
  4995. $this->count_submissions_need_grading($activitygroup),
  4996. $instance->teamsubmission,
  4997. assign_grading_summary::WARN_GROUPS_NO,
  4998. $course->relativedatesmode,
  4999. $course->startdate,
  5000. $this->can_grade(),
  5001. $isvisible
  5002. );
  5003. }
  5004. return $summary;
  5005. }
  5006. /**
  5007. * Return group override duedate.
  5008. *
  5009. * @param int $activitygroup Activity active group
  5010. * @return int $duedate
  5011. */
  5012. private function get_duedate($activitygroup = null) {
  5013. global $DB;
  5014. if ($activitygroup === null) {
  5015. $activitygroup = groups_get_activity_group($this->get_course_module());
  5016. }
  5017. if ($this->can_view_grades()) {
  5018. $params = array('groupid' => $activitygroup, 'assignid' => $this->get_instance()->id);
  5019. $groupoverride = $DB->get_record('assign_overrides', $params);
  5020. if (!empty($groupoverride->duedate)) {
  5021. return $groupoverride->duedate;
  5022. }
  5023. }
  5024. return $this->get_instance()->duedate;
  5025. }
  5026. /**
  5027. * View submissions page (contains details of current submission).
  5028. *
  5029. * @return string
  5030. */
  5031. protected function view_submission_page() {
  5032. global $CFG, $DB, $USER, $PAGE;
  5033. $instance = $this->get_instance();
  5034. $this->add_grade_notices();
  5035. $o = '';
  5036. $postfix = '';
  5037. if ($this->has_visible_attachments()) {
  5038. $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
  5039. }
  5040. $o .= $this->get_renderer()->render(new assign_header($instance,
  5041. $this->get_context(),
  5042. $this->show_intro(),
  5043. $this->get_course_module()->id,
  5044. '', '', $postfix));
  5045. // Display plugin specific headers.
  5046. $plugins = array_merge($this->get_submission_plugins(), $this->get_feedback_plugins());
  5047. foreach ($plugins as $plugin) {
  5048. if ($plugin->is_enabled() && $plugin->is_visible()) {
  5049. $o .= $this->get_renderer()->render(new assign_plugin_header($plugin));
  5050. }
  5051. }
  5052. if ($this->can_view_grades()) {
  5053. // Group selector will only be displayed if necessary.
  5054. $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
  5055. $o .= groups_print_activity_menu($this->get_course_module(), $currenturl->out(), true);
  5056. $summary = $this->get_assign_grading_summary_renderable();
  5057. $o .= $this->get_renderer()->render($summary);
  5058. }
  5059. $grade = $this->get_user_grade($USER->id, false);
  5060. $submission = $this->get_user_submission($USER->id, false);
  5061. if ($this->can_view_submission($USER->id)) {
  5062. $o .= $this->view_student_summary($USER, true);
  5063. }
  5064. $o .= $this->view_footer();
  5065. \mod_assign\event\submission_status_viewed::create_from_assign($this)->trigger();
  5066. return $o;
  5067. }
  5068. /**
  5069. * Convert the final raw grade(s) in the grading table for the gradebook.
  5070. *
  5071. * @param stdClass $grade
  5072. * @return array
  5073. */
  5074. protected function convert_grade_for_gradebook(stdClass $grade) {
  5075. $gradebookgrade = array();
  5076. if ($grade->grade >= 0) {
  5077. $gradebookgrade['rawgrade'] = $grade->grade;
  5078. }
  5079. // Allow "no grade" to be chosen.
  5080. if ($grade->grade == -1) {
  5081. $gradebookgrade['rawgrade'] = NULL;
  5082. }
  5083. $gradebookgrade['userid'] = $grade->userid;
  5084. $gradebookgrade['usermodified'] = $grade->grader;
  5085. $gradebookgrade['datesubmitted'] = null;
  5086. $gradebookgrade['dategraded'] = $grade->timemodified;
  5087. if (isset($grade->feedbackformat)) {
  5088. $gradebookgrade['feedbackformat'] = $grade->feedbackformat;
  5089. }
  5090. if (isset($grade->feedbacktext)) {
  5091. $gradebookgrade['feedback'] = $grade->feedbacktext;
  5092. }
  5093. if (isset($grade->feedbackfiles)) {
  5094. $gradebookgrade['feedbackfiles'] = $grade->feedbackfiles;
  5095. }
  5096. return $gradebookgrade;
  5097. }
  5098. /**
  5099. * Convert submission details for the gradebook.
  5100. *
  5101. * @param stdClass $submission
  5102. * @return array
  5103. */
  5104. protected function convert_submission_for_gradebook(stdClass $submission) {
  5105. $gradebookgrade = array();
  5106. $gradebookgrade['userid'] = $submission->userid;
  5107. $gradebookgrade['usermodified'] = $submission->userid;
  5108. $gradebookgrade['datesubmitted'] = $submission->timemodified;
  5109. return $gradebookgrade;
  5110. }
  5111. /**
  5112. * Update grades in the gradebook.
  5113. *
  5114. * @param mixed $submission stdClass|null
  5115. * @param mixed $grade stdClass|null
  5116. * @return bool
  5117. */
  5118. protected function gradebook_item_update($submission=null, $grade=null) {
  5119. global $CFG;
  5120. require_once($CFG->dirroot.'/mod/assign/lib.php');
  5121. // Do not push grade to gradebook if blind marking is active as
  5122. // the gradebook would reveal the students.
  5123. if ($this->is_blind_marking()) {
  5124. return false;
  5125. }
  5126. // If marking workflow is enabled and grade is not released then remove any grade that may exist in the gradebook.
  5127. if ($this->get_instance()->markingworkflow && !empty($grade) &&
  5128. $this->get_grading_status($grade->userid) != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
  5129. // Remove the grade (if it exists) from the gradebook as it is not 'final'.
  5130. $grade->grade = -1;
  5131. $grade->feedbacktext = '';
  5132. $grade->feebackfiles = [];
  5133. }
  5134. if ($submission != null) {
  5135. if ($submission->userid == 0) {
  5136. // This is a group submission update.
  5137. $team = groups_get_members($submission->groupid, 'u.id');
  5138. foreach ($team as $member) {
  5139. $membersubmission = clone $submission;
  5140. $membersubmission->groupid = 0;
  5141. $membersubmission->userid = $member->id;
  5142. $this->gradebook_item_update($membersubmission, null);
  5143. }
  5144. return;
  5145. }
  5146. $gradebookgrade = $this->convert_submission_for_gradebook($submission);
  5147. } else {
  5148. $gradebookgrade = $this->convert_grade_for_gradebook($grade);
  5149. }
  5150. // Grading is disabled, return.
  5151. if ($this->grading_disabled($gradebookgrade['userid'])) {
  5152. return false;
  5153. }
  5154. $assign = clone $this->get_instance();
  5155. $assign->cmidnumber = $this->get_course_module()->idnumber;
  5156. // Set assign gradebook feedback plugin status (enabled and visible).
  5157. $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
  5158. return assign_grade_item_update($assign, $gradebookgrade) == GRADE_UPDATE_OK;
  5159. }
  5160. /**
  5161. * Update team submission.
  5162. *
  5163. * @param stdClass $submission
  5164. * @param int $userid
  5165. * @param bool $updatetime
  5166. * @return bool
  5167. */
  5168. protected function update_team_submission(stdClass $submission, $userid, $updatetime) {
  5169. global $DB;
  5170. if ($updatetime) {
  5171. $submission->timemodified = time();
  5172. }
  5173. // First update the submission for the current user.
  5174. $mysubmission = $this->get_user_submission($userid, true, $submission->attemptnumber);
  5175. $mysubmission->status = $submission->status;
  5176. $this->update_submission($mysubmission, 0, $updatetime, false);
  5177. // Now check the team settings to see if this assignment qualifies as submitted or draft.
  5178. $team = $this->get_submission_group_members($submission->groupid, true);
  5179. $allsubmitted = true;
  5180. $anysubmitted = false;
  5181. $result = true;
  5182. if (!in_array($submission->status, [ASSIGN_SUBMISSION_STATUS_NEW, ASSIGN_SUBMISSION_STATUS_REOPENED])) {
  5183. foreach ($team as $member) {
  5184. $membersubmission = $this->get_user_submission($member->id, false, $submission->attemptnumber);
  5185. // If no submission found for team member and member is active then everyone has not submitted.
  5186. if (!$membersubmission || $membersubmission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED
  5187. && ($this->is_active_user($member->id))) {
  5188. $allsubmitted = false;
  5189. if ($anysubmitted) {
  5190. break;
  5191. }
  5192. } else {
  5193. $anysubmitted = true;
  5194. }
  5195. }
  5196. if ($this->get_instance()->requireallteammemberssubmit) {
  5197. if ($allsubmitted) {
  5198. $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
  5199. } else {
  5200. $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
  5201. }
  5202. $result = $DB->update_record('assign_submission', $submission);
  5203. } else {
  5204. if ($anysubmitted) {
  5205. $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
  5206. } else {
  5207. $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
  5208. }
  5209. $result = $DB->update_record('assign_submission', $submission);
  5210. }
  5211. } else {
  5212. // Set the group submission to reopened.
  5213. foreach ($team as $member) {
  5214. $membersubmission = $this->get_user_submission($member->id, true, $submission->attemptnumber);
  5215. $membersubmission->status = $submission->status;
  5216. $result = $DB->update_record('assign_submission', $membersubmission) && $result;
  5217. }
  5218. $result = $DB->update_record('assign_submission', $submission) && $result;
  5219. }
  5220. $this->gradebook_item_update($submission);
  5221. return $result;
  5222. }
  5223. /**
  5224. * Update grades in the gradebook based on submission time.
  5225. *
  5226. * @param stdClass $submission
  5227. * @param int $userid
  5228. * @param bool $updatetime
  5229. * @param bool $teamsubmission
  5230. * @return bool
  5231. */
  5232. protected function update_submission(stdClass $submission, $userid, $updatetime, $teamsubmission) {
  5233. global $DB;
  5234. if ($teamsubmission) {
  5235. return $this->update_team_submission($submission, $userid, $updatetime);
  5236. }
  5237. if ($updatetime) {
  5238. $submission->timemodified = time();
  5239. }
  5240. $result= $DB->update_record('assign_submission', $submission);
  5241. if ($result) {
  5242. $this->gradebook_item_update($submission);
  5243. }
  5244. return $result;
  5245. }
  5246. /**
  5247. * Is this assignment open for submissions?
  5248. *
  5249. * Check the due date,
  5250. * prevent late submissions,
  5251. * has this person already submitted,
  5252. * is the assignment locked?
  5253. *
  5254. * @param int $userid - Optional userid so we can see if a different user can submit
  5255. * @param bool $skipenrolled - Skip enrollment checks (because they have been done already)
  5256. * @param stdClass $submission - Pre-fetched submission record (or false to fetch it)
  5257. * @param stdClass $flags - Pre-fetched user flags record (or false to fetch it)
  5258. * @param stdClass $gradinginfo - Pre-fetched user gradinginfo record (or false to fetch it)
  5259. * @return bool
  5260. */
  5261. public function submissions_open($userid = 0,
  5262. $skipenrolled = false,
  5263. $submission = false,
  5264. $flags = false,
  5265. $gradinginfo = false) {
  5266. global $USER;
  5267. if (!$userid) {
  5268. $userid = $USER->id;
  5269. }
  5270. $time = time();
  5271. $dateopen = true;
  5272. $finaldate = false;
  5273. if ($this->get_instance()->cutoffdate) {
  5274. $finaldate = $this->get_instance()->cutoffdate;
  5275. }
  5276. if ($flags === false) {
  5277. $flags = $this->get_user_flags($userid, false);
  5278. }
  5279. if ($flags && $flags->locked) {
  5280. return false;
  5281. }
  5282. // User extensions.
  5283. if ($finaldate) {
  5284. if ($flags && $flags->extensionduedate) {
  5285. // Extension can be before cut off date.
  5286. if ($flags->extensionduedate > $finaldate) {
  5287. $finaldate = $flags->extensionduedate;
  5288. }
  5289. }
  5290. }
  5291. if ($finaldate) {
  5292. $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time && $time <= $finaldate);
  5293. } else {
  5294. $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time);
  5295. }
  5296. if (!$dateopen) {
  5297. return false;
  5298. }
  5299. // Now check if this user has already submitted etc.
  5300. if (!$skipenrolled && !is_enrolled($this->get_course_context(), $userid)) {
  5301. return false;
  5302. }
  5303. // Note you can pass null for submission and it will not be fetched.
  5304. if ($submission === false) {
  5305. if ($this->get_instance()->teamsubmission) {
  5306. $submission = $this->get_group_submission($userid, 0, false);
  5307. } else {
  5308. $submission = $this->get_user_submission($userid, false);
  5309. }
  5310. }
  5311. if ($submission) {
  5312. if ($this->get_instance()->submissiondrafts && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
  5313. // Drafts are tracked and the student has submitted the assignment.
  5314. return false;
  5315. }
  5316. }
  5317. // See if this user grade is locked in the gradebook.
  5318. if ($gradinginfo === false) {
  5319. $gradinginfo = grade_get_grades($this->get_course()->id,
  5320. 'mod',
  5321. 'assign',
  5322. $this->get_instance()->id,
  5323. array($userid));
  5324. }
  5325. if ($gradinginfo &&
  5326. isset($gradinginfo->items[0]->grades[$userid]) &&
  5327. $gradinginfo->items[0]->grades[$userid]->locked) {
  5328. return false;
  5329. }
  5330. return true;
  5331. }
  5332. /**
  5333. * Render the files in file area.
  5334. *
  5335. * @param string $component
  5336. * @param string $area
  5337. * @param int $submissionid
  5338. * @return string
  5339. */
  5340. public function render_area_files($component, $area, $submissionid) {
  5341. global $USER;
  5342. return $this->get_renderer()->assign_files($this->context, $submissionid, $area, $component,
  5343. $this->course, $this->coursemodule);
  5344. }
  5345. /**
  5346. * Capability check to make sure this grader can edit this submission.
  5347. *
  5348. * @param int $userid - The user whose submission is to be edited
  5349. * @param int $graderid (optional) - The user who will do the editing (default to $USER->id).
  5350. * @return bool
  5351. */
  5352. public function can_edit_submission($userid, $graderid = 0) {
  5353. global $USER;
  5354. if (empty($graderid)) {
  5355. $graderid = $USER->id;
  5356. }
  5357. $instance = $this->get_instance();
  5358. if ($userid == $graderid &&
  5359. $instance->teamsubmission &&
  5360. $instance->preventsubmissionnotingroup &&
  5361. $this->get_submission_group($userid) == false) {
  5362. return false;
  5363. }
  5364. if ($userid == $graderid) {
  5365. if ($this->submissions_open($userid) &&
  5366. has_capability('mod/assign:submit', $this->context, $graderid)) {
  5367. // User can edit their own submission.
  5368. return true;
  5369. } else {
  5370. // We need to return here because editothersubmission should never apply to a users own submission.
  5371. return false;
  5372. }
  5373. }
  5374. if (!has_capability('mod/assign:editothersubmission', $this->context, $graderid)) {
  5375. return false;
  5376. }
  5377. $cm = $this->get_course_module();
  5378. if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
  5379. $sharedgroupmembers = $this->get_shared_group_members($cm, $graderid);
  5380. return in_array($userid, $sharedgroupmembers);
  5381. }
  5382. return true;
  5383. }
  5384. /**
  5385. * Returns IDs of the users who share group membership with the specified user.
  5386. *
  5387. * @param stdClass|cm_info $cm Course-module
  5388. * @param int $userid User ID
  5389. * @return array An array of ID of users.
  5390. */
  5391. public function get_shared_group_members($cm, $userid) {
  5392. if (!isset($this->sharedgroupmembers[$userid])) {
  5393. $this->sharedgroupmembers[$userid] = array();
  5394. if ($members = groups_get_activity_shared_group_members($cm, $userid)) {
  5395. $this->sharedgroupmembers[$userid] = array_keys($members);
  5396. }
  5397. }
  5398. return $this->sharedgroupmembers[$userid];
  5399. }
  5400. /**
  5401. * Returns a list of teachers that should be grading given submission.
  5402. *
  5403. * @param int $userid The submission to grade
  5404. * @return array
  5405. */
  5406. protected function get_graders($userid) {
  5407. // Potential graders should be active users only.
  5408. $potentialgraders = get_enrolled_users($this->context, "mod/assign:grade", null, 'u.*', null, null, null, true);
  5409. $graders = array();
  5410. if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
  5411. if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
  5412. foreach ($groups as $group) {
  5413. foreach ($potentialgraders as $grader) {
  5414. if ($grader->id == $userid) {
  5415. // Do not send self.
  5416. continue;
  5417. }
  5418. if (groups_is_member($group->id, $grader->id)) {
  5419. $graders[$grader->id] = $grader;
  5420. }
  5421. }
  5422. }
  5423. } else {
  5424. // User not in group, try to find graders without group.
  5425. foreach ($potentialgraders as $grader) {
  5426. if ($grader->id == $userid) {
  5427. // Do not send self.
  5428. continue;
  5429. }
  5430. if (!groups_has_membership($this->get_course_module(), $grader->id)) {
  5431. $graders[$grader->id] = $grader;
  5432. }
  5433. }
  5434. }
  5435. } else {
  5436. foreach ($potentialgraders as $grader) {
  5437. if ($grader->id == $userid) {
  5438. // Do not send self.
  5439. continue;
  5440. }
  5441. // Must be enrolled.
  5442. if (is_enrolled($this->get_course_context(), $grader->id)) {
  5443. $graders[$grader->id] = $grader;
  5444. }
  5445. }
  5446. }
  5447. return $graders;
  5448. }
  5449. /**
  5450. * Returns a list of users that should receive notification about given submission.
  5451. *
  5452. * @param int $userid The submission to grade
  5453. * @return array
  5454. */
  5455. protected function get_notifiable_users($userid) {
  5456. // Potential users should be active users only.
  5457. $potentialusers = get_enrolled_users($this->context, "mod/assign:receivegradernotifications",
  5458. null, 'u.*', null, null, null, true);
  5459. $notifiableusers = array();
  5460. if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
  5461. if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
  5462. foreach ($groups as $group) {
  5463. foreach ($potentialusers as $potentialuser) {
  5464. if ($potentialuser->id == $userid) {
  5465. // Do not send self.
  5466. continue;
  5467. }
  5468. if (groups_is_member($group->id, $potentialuser->id)) {
  5469. $notifiableusers[$potentialuser->id] = $potentialuser;
  5470. }
  5471. }
  5472. }
  5473. } else {
  5474. // User not in group, try to find graders without group.
  5475. foreach ($potentialusers as $potentialuser) {
  5476. if ($potentialuser->id == $userid) {
  5477. // Do not send self.
  5478. continue;
  5479. }
  5480. if (!groups_has_membership($this->get_course_module(), $potentialuser->id)) {
  5481. $notifiableusers[$potentialuser->id] = $potentialuser;
  5482. }
  5483. }
  5484. }
  5485. } else {
  5486. foreach ($potentialusers as $potentialuser) {
  5487. if ($potentialuser->id == $userid) {
  5488. // Do not send self.
  5489. continue;
  5490. }
  5491. // Must be enrolled.
  5492. if (is_enrolled($this->get_course_context(), $potentialuser->id)) {
  5493. $notifiableusers[$potentialuser->id] = $potentialuser;
  5494. }
  5495. }
  5496. }
  5497. return $notifiableusers;
  5498. }
  5499. /**
  5500. * Format a notification for plain text.
  5501. *
  5502. * @param string $messagetype
  5503. * @param stdClass $info
  5504. * @param stdClass $course
  5505. * @param stdClass $context
  5506. * @param string $modulename
  5507. * @param string $assignmentname
  5508. */
  5509. protected static function format_notification_message_text($messagetype,
  5510. $info,
  5511. $course,
  5512. $context,
  5513. $modulename,
  5514. $assignmentname) {
  5515. $formatparams = array('context' => $context->get_course_context());
  5516. $posttext = format_string($course->shortname, true, $formatparams) .
  5517. ' -> ' .
  5518. $modulename .
  5519. ' -> ' .
  5520. format_string($assignmentname, true, $formatparams) . "\n";
  5521. $posttext .= '---------------------------------------------------------------------' . "\n";
  5522. $posttext .= get_string($messagetype . 'text', 'assign', $info)."\n";
  5523. $posttext .= "\n---------------------------------------------------------------------\n";
  5524. return $posttext;
  5525. }
  5526. /**
  5527. * Format a notification for HTML.
  5528. *
  5529. * @param string $messagetype
  5530. * @param stdClass $info
  5531. * @param stdClass $course
  5532. * @param stdClass $context
  5533. * @param string $modulename
  5534. * @param stdClass $coursemodule
  5535. * @param string $assignmentname
  5536. */
  5537. protected static function format_notification_message_html($messagetype,
  5538. $info,
  5539. $course,
  5540. $context,
  5541. $modulename,
  5542. $coursemodule,
  5543. $assignmentname) {
  5544. global $CFG;
  5545. $formatparams = array('context' => $context->get_course_context());
  5546. $posthtml = '<p><font face="sans-serif">' .
  5547. '<a href="' . $CFG->wwwroot . '/course/view.php?id=' . $course->id . '">' .
  5548. format_string($course->shortname, true, $formatparams) .
  5549. '</a> ->' .
  5550. '<a href="' . $CFG->wwwroot . '/mod/assign/index.php?id=' . $course->id . '">' .
  5551. $modulename .
  5552. '</a> ->' .
  5553. '<a href="' . $CFG->wwwroot . '/mod/assign/view.php?id=' . $coursemodule->id . '">' .
  5554. format_string($assignmentname, true, $formatparams) .
  5555. '</a></font></p>';
  5556. $posthtml .= '<hr /><font face="sans-serif">';
  5557. $posthtml .= '<p>' . get_string($messagetype . 'html', 'assign', $info) . '</p>';
  5558. $posthtml .= '</font><hr />';
  5559. return $posthtml;
  5560. }
  5561. /**
  5562. * Message someone about something (static so it can be called from cron).
  5563. *
  5564. * @param stdClass $userfrom
  5565. * @param stdClass $userto
  5566. * @param string $messagetype
  5567. * @param string $eventtype
  5568. * @param int $updatetime
  5569. * @param stdClass $coursemodule
  5570. * @param stdClass $context
  5571. * @param stdClass $course
  5572. * @param string $modulename
  5573. * @param string $assignmentname
  5574. * @param bool $blindmarking
  5575. * @param int $uniqueidforuser
  5576. * @return void
  5577. */
  5578. public static function send_assignment_notification($userfrom,
  5579. $userto,
  5580. $messagetype,
  5581. $eventtype,
  5582. $updatetime,
  5583. $coursemodule,
  5584. $context,
  5585. $course,
  5586. $modulename,
  5587. $assignmentname,
  5588. $blindmarking,
  5589. $uniqueidforuser) {
  5590. global $CFG, $PAGE;
  5591. $info = new stdClass();
  5592. if ($blindmarking) {
  5593. $userfrom = clone($userfrom);
  5594. $info->username = get_string('participant', 'assign') . ' ' . $uniqueidforuser;
  5595. $userfrom->firstname = get_string('participant', 'assign');
  5596. $userfrom->lastname = $uniqueidforuser;
  5597. $userfrom->email = $CFG->noreplyaddress;
  5598. } else {
  5599. $info->username = fullname($userfrom, true);
  5600. }
  5601. $info->assignment = format_string($assignmentname, true, array('context'=>$context));
  5602. $info->url = $CFG->wwwroot.'/mod/assign/view.php?id='.$coursemodule->id;
  5603. $info->timeupdated = userdate($updatetime, get_string('strftimerecentfull'));
  5604. $postsubject = get_string($messagetype . 'small', 'assign', $info);
  5605. $posttext = self::format_notification_message_text($messagetype,
  5606. $info,
  5607. $course,
  5608. $context,
  5609. $modulename,
  5610. $assignmentname);
  5611. $posthtml = '';
  5612. if ($userto->mailformat == 1) {
  5613. $posthtml = self::format_notification_message_html($messagetype,
  5614. $info,
  5615. $course,
  5616. $context,
  5617. $modulename,
  5618. $coursemodule,
  5619. $assignmentname);
  5620. }
  5621. $eventdata = new \core\message\message();
  5622. $eventdata->courseid = $course->id;
  5623. $eventdata->modulename = 'assign';
  5624. $eventdata->userfrom = $userfrom;
  5625. $eventdata->userto = $userto;
  5626. $eventdata->subject = $postsubject;
  5627. $eventdata->fullmessage = $posttext;
  5628. $eventdata->fullmessageformat = FORMAT_PLAIN;
  5629. $eventdata->fullmessagehtml = $posthtml;
  5630. $eventdata->smallmessage = $postsubject;
  5631. $eventdata->name = $eventtype;
  5632. $eventdata->component = 'mod_assign';
  5633. $eventdata->notification = 1;
  5634. $eventdata->contexturl = $info->url;
  5635. $eventdata->contexturlname = $info->assignment;
  5636. $customdata = [
  5637. 'cmid' => $coursemodule->id,
  5638. 'instance' => $coursemodule->instance,
  5639. 'messagetype' => $messagetype,
  5640. 'blindmarking' => $blindmarking,
  5641. 'uniqueidforuser' => $uniqueidforuser,
  5642. ];
  5643. // Check if the userfrom is real and visible.
  5644. if (!empty($userfrom->id) && core_user::is_real_user($userfrom->id)) {
  5645. $userpicture = new user_picture($userfrom);
  5646. $userpicture->size = 1; // Use f1 size.
  5647. $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
  5648. $customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false);
  5649. }
  5650. $eventdata->customdata = $customdata;
  5651. message_send($eventdata);
  5652. }
  5653. /**
  5654. * Message someone about something.
  5655. *
  5656. * @param stdClass $userfrom
  5657. * @param stdClass $userto
  5658. * @param string $messagetype
  5659. * @param string $eventtype
  5660. * @param int $updatetime
  5661. * @return void
  5662. */
  5663. public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime) {
  5664. global $USER;
  5665. $userid = core_user::is_real_user($userfrom->id) ? $userfrom->id : $USER->id;
  5666. $uniqueid = $this->get_uniqueid_for_user($userid);
  5667. self::send_assignment_notification($userfrom,
  5668. $userto,
  5669. $messagetype,
  5670. $eventtype,
  5671. $updatetime,
  5672. $this->get_course_module(),
  5673. $this->get_context(),
  5674. $this->get_course(),
  5675. $this->get_module_name(),
  5676. $this->get_instance()->name,
  5677. $this->is_blind_marking(),
  5678. $uniqueid);
  5679. }
  5680. /**
  5681. * Notify student upon successful submission copy.
  5682. *
  5683. * @param stdClass $submission
  5684. * @return void
  5685. */
  5686. protected function notify_student_submission_copied(stdClass $submission) {
  5687. global $DB, $USER;
  5688. $adminconfig = $this->get_admin_config();
  5689. // Use the same setting for this - no need for another one.
  5690. if (empty($adminconfig->submissionreceipts)) {
  5691. // No need to do anything.
  5692. return;
  5693. }
  5694. if ($submission->userid) {
  5695. $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
  5696. } else {
  5697. $user = $USER;
  5698. }
  5699. $this->send_notification($user,
  5700. $user,
  5701. 'submissioncopied',
  5702. 'assign_notification',
  5703. $submission->timemodified);
  5704. }
  5705. /**
  5706. * Notify student upon successful submission.
  5707. *
  5708. * @param stdClass $submission
  5709. * @return void
  5710. */
  5711. protected function notify_student_submission_receipt(stdClass $submission) {
  5712. global $DB, $USER;
  5713. $adminconfig = $this->get_admin_config();
  5714. if (empty($adminconfig->submissionreceipts)) {
  5715. // No need to do anything.
  5716. return;
  5717. }
  5718. if ($submission->userid) {
  5719. $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
  5720. } else {
  5721. $user = $USER;
  5722. }
  5723. if ($submission->userid == $USER->id) {
  5724. $this->send_notification(core_user::get_noreply_user(),
  5725. $user,
  5726. 'submissionreceipt',
  5727. 'assign_notification',
  5728. $submission->timemodified);
  5729. } else {
  5730. $this->send_notification($USER,
  5731. $user,
  5732. 'submissionreceiptother',
  5733. 'assign_notification',
  5734. $submission->timemodified);
  5735. }
  5736. }
  5737. /**
  5738. * Send notifications to graders upon student submissions.
  5739. *
  5740. * @param stdClass $submission
  5741. * @return void
  5742. */
  5743. protected function notify_graders(stdClass $submission) {
  5744. global $DB, $USER;
  5745. $instance = $this->get_instance();
  5746. $late = $instance->duedate && ($instance->duedate < time());
  5747. if (!$instance->sendnotifications && !($late && $instance->sendlatenotifications)) {
  5748. // No need to do anything.
  5749. return;
  5750. }
  5751. if ($submission->userid) {
  5752. $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
  5753. } else {
  5754. $user = $USER;
  5755. }
  5756. if ($notifyusers = $this->get_notifiable_users($user->id)) {
  5757. foreach ($notifyusers as $notifyuser) {
  5758. $this->send_notification($user,
  5759. $notifyuser,
  5760. 'gradersubmissionupdated',
  5761. 'assign_notification',
  5762. $submission->timemodified);
  5763. }
  5764. }
  5765. }
  5766. /**
  5767. * Submit a submission for grading.
  5768. *
  5769. * @param stdClass $data - The form data
  5770. * @param array $notices - List of error messages to display on an error condition.
  5771. * @return bool Return false if the submission was not submitted.
  5772. */
  5773. public function submit_for_grading($data, $notices) {
  5774. global $USER;
  5775. $userid = $USER->id;
  5776. if (!empty($data->userid)) {
  5777. $userid = $data->userid;
  5778. }
  5779. // Need submit permission to submit an assignment.
  5780. if ($userid == $USER->id) {
  5781. require_capability('mod/assign:submit', $this->context);
  5782. } else {
  5783. if (!$this->can_edit_submission($userid, $USER->id)) {
  5784. print_error('nopermission');
  5785. }
  5786. }
  5787. $instance = $this->get_instance();
  5788. if ($instance->teamsubmission) {
  5789. $submission = $this->get_group_submission($userid, 0, true);
  5790. } else {
  5791. $submission = $this->get_user_submission($userid, true);
  5792. }
  5793. if (!$this->submissions_open($userid)) {
  5794. $notices[] = get_string('submissionsclosed', 'assign');
  5795. return false;
  5796. }
  5797. if ($instance->requiresubmissionstatement && empty($data->submissionstatement) && $USER->id == $userid) {
  5798. return false;
  5799. }
  5800. if ($submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
  5801. // Give each submission plugin a chance to process the submission.
  5802. $plugins = $this->get_submission_plugins();
  5803. foreach ($plugins as $plugin) {
  5804. if ($plugin->is_enabled() && $plugin->is_visible()) {
  5805. $plugin->submit_for_grading($submission);
  5806. }
  5807. }
  5808. $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
  5809. $this->update_submission($submission, $userid, true, $instance->teamsubmission);
  5810. $completion = new completion_info($this->get_course());
  5811. if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
  5812. $this->update_activity_completion_records($instance->teamsubmission,
  5813. $instance->requireallteammemberssubmit,
  5814. $submission,
  5815. $userid,
  5816. COMPLETION_COMPLETE,
  5817. $completion);
  5818. }
  5819. if (!empty($data->submissionstatement) && $USER->id == $userid) {
  5820. \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
  5821. }
  5822. $this->notify_graders($submission);
  5823. $this->notify_student_submission_receipt($submission);
  5824. \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, false)->trigger();
  5825. return true;
  5826. }
  5827. $notices[] = get_string('submissionsclosed', 'assign');
  5828. return false;
  5829. }
  5830. /**
  5831. * A students submission is submitted for grading by a teacher.
  5832. *
  5833. * @return bool
  5834. */
  5835. protected function process_submit_other_for_grading($mform, $notices) {
  5836. global $USER, $CFG;
  5837. require_sesskey();
  5838. $userid = optional_param('userid', $USER->id, PARAM_INT);
  5839. if (!$this->submissions_open($userid)) {
  5840. $notices[] = get_string('submissionsclosed', 'assign');
  5841. return false;
  5842. }
  5843. $data = new stdClass();
  5844. $data->userid = $userid;
  5845. return $this->submit_for_grading($data, $notices);
  5846. }
  5847. /**
  5848. * Assignment submission is processed before grading.
  5849. *
  5850. * @param moodleform|null $mform If validation failed when submitting this form - this is the moodleform.
  5851. * It can be null.
  5852. * @return bool Return false if the validation fails. This affects which page is displayed next.
  5853. */
  5854. protected function process_submit_for_grading($mform, $notices) {
  5855. global $CFG;
  5856. require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
  5857. require_sesskey();
  5858. if (!$this->submissions_open()) {
  5859. $notices[] = get_string('submissionsclosed', 'assign');
  5860. return false;
  5861. }
  5862. $data = new stdClass();
  5863. $adminconfig = $this->get_admin_config();
  5864. $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
  5865. $submissionstatement = '';
  5866. if ($requiresubmissionstatement) {
  5867. $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
  5868. }
  5869. // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
  5870. // that the submission statement checkbox will be displayed.
  5871. if (empty($submissionstatement)) {
  5872. $requiresubmissionstatement = false;
  5873. }
  5874. if ($mform == null) {
  5875. $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
  5876. $submissionstatement,
  5877. $this->get_course_module()->id,
  5878. $data));
  5879. }
  5880. $data = $mform->get_data();
  5881. if (!$mform->is_cancelled()) {
  5882. if ($mform->get_data() == false) {
  5883. return false;
  5884. }
  5885. return $this->submit_for_grading($data, $notices);
  5886. }
  5887. return true;
  5888. }
  5889. /**
  5890. * Save the extension date for a single user.
  5891. *
  5892. * @param int $userid The user id
  5893. * @param mixed $extensionduedate Either an integer date or null
  5894. * @return boolean
  5895. */
  5896. public function save_user_extension($userid, $extensionduedate) {
  5897. global $DB;
  5898. // Need submit permission to submit an assignment.
  5899. require_capability('mod/assign:grantextension', $this->context);
  5900. if (!is_enrolled($this->get_course_context(), $userid)) {
  5901. return false;
  5902. }
  5903. if (!has_capability('mod/assign:submit', $this->context, $userid)) {
  5904. return false;
  5905. }
  5906. if ($this->get_instance()->duedate && $extensionduedate) {
  5907. if ($this->get_instance()->duedate > $extensionduedate) {
  5908. return false;
  5909. }
  5910. }
  5911. if ($this->get_instance()->allowsubmissionsfromdate && $extensionduedate) {
  5912. if ($this->get_instance()->allowsubmissionsfromdate > $extensionduedate) {
  5913. return false;
  5914. }
  5915. }
  5916. $flags = $this->get_user_flags($userid, true);
  5917. $flags->extensionduedate = $extensionduedate;
  5918. $result = $this->update_user_flags($flags);
  5919. if ($result) {
  5920. \mod_assign\event\extension_granted::create_from_assign($this, $userid)->trigger();
  5921. }
  5922. return $result;
  5923. }
  5924. /**
  5925. * Save extension date.
  5926. *
  5927. * @param moodleform $mform The submitted form
  5928. * @return boolean
  5929. */
  5930. protected function process_save_extension(& $mform) {
  5931. global $DB, $CFG;
  5932. // Include extension form.
  5933. require_once($CFG->dirroot . '/mod/assign/extensionform.php');
  5934. require_sesskey();
  5935. $users = optional_param('userid', 0, PARAM_INT);
  5936. if (!$users) {
  5937. $users = required_param('selectedusers', PARAM_SEQUENCE);
  5938. }
  5939. $userlist = explode(',', $users);
  5940. $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
  5941. $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
  5942. foreach ($userlist as $userid) {
  5943. // To validate extension date with users overrides.
  5944. $override = $this->override_exists($userid);
  5945. foreach ($keys as $key) {
  5946. if ($override->{$key}) {
  5947. if ($maxoverride[$key] < $override->{$key}) {
  5948. $maxoverride[$key] = $override->{$key};
  5949. }
  5950. } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
  5951. $maxoverride[$key] = $this->get_instance()->{$key};
  5952. }
  5953. }
  5954. }
  5955. foreach ($keys as $key) {
  5956. if ($maxoverride[$key]) {
  5957. $this->get_instance()->{$key} = $maxoverride[$key];
  5958. }
  5959. }
  5960. $formparams = array(
  5961. 'instance' => $this->get_instance(),
  5962. 'assign' => $this,
  5963. 'userlist' => $userlist
  5964. );
  5965. $mform = new mod_assign_extension_form(null, $formparams);
  5966. if ($mform->is_cancelled()) {
  5967. return true;
  5968. }
  5969. if ($formdata = $mform->get_data()) {
  5970. if (!empty($formdata->selectedusers)) {
  5971. $users = explode(',', $formdata->selectedusers);
  5972. $result = true;
  5973. foreach ($users as $userid) {
  5974. $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
  5975. $result = $this->save_user_extension($user->id, $formdata->extensionduedate) && $result;
  5976. }
  5977. return $result;
  5978. }
  5979. if (!empty($formdata->userid)) {
  5980. $user = $DB->get_record('user', array('id' => $formdata->userid), '*', MUST_EXIST);
  5981. return $this->save_user_extension($user->id, $formdata->extensionduedate);
  5982. }
  5983. }
  5984. return false;
  5985. }
  5986. /**
  5987. * Save quick grades.
  5988. *
  5989. * @return string The result of the save operation
  5990. */
  5991. protected function process_save_quick_grades() {
  5992. global $USER, $DB, $CFG;
  5993. // Need grade permission.
  5994. require_capability('mod/assign:grade', $this->context);
  5995. require_sesskey();
  5996. // Make sure advanced grading is disabled.
  5997. $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
  5998. $controller = $gradingmanager->get_active_controller();
  5999. if (!empty($controller)) {
  6000. $message = get_string('errorquickgradingvsadvancedgrading', 'assign');
  6001. $this->set_error_message($message);
  6002. return $message;
  6003. }
  6004. $users = array();
  6005. // First check all the last modified values.
  6006. $currentgroup = groups_get_activity_group($this->get_course_module(), true);
  6007. $participants = $this->list_participants($currentgroup, true);
  6008. // Gets a list of possible users and look for values based upon that.
  6009. foreach ($participants as $userid => $unused) {
  6010. $modified = optional_param('grademodified_' . $userid, -1, PARAM_INT);
  6011. $attemptnumber = optional_param('gradeattempt_' . $userid, -1, PARAM_INT);
  6012. // Gather the userid, updated grade and last modified value.
  6013. $record = new stdClass();
  6014. $record->userid = $userid;
  6015. if ($modified >= 0) {
  6016. $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT));
  6017. $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_ALPHA);
  6018. $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT);
  6019. } else {
  6020. // This user was not in the grading table.
  6021. continue;
  6022. }
  6023. $record->attemptnumber = $attemptnumber;
  6024. $record->lastmodified = $modified;
  6025. $record->gradinginfo = grade_get_grades($this->get_course()->id,
  6026. 'mod',
  6027. 'assign',
  6028. $this->get_instance()->id,
  6029. array($userid));
  6030. $users[$userid] = $record;
  6031. }
  6032. if (empty($users)) {
  6033. $message = get_string('nousersselected', 'assign');
  6034. $this->set_error_message($message);
  6035. return $message;
  6036. }
  6037. list($userids, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED);
  6038. $params['assignid1'] = $this->get_instance()->id;
  6039. $params['assignid2'] = $this->get_instance()->id;
  6040. // Check them all for currency.
  6041. $grademaxattempt = 'SELECT s.userid, s.attemptnumber AS maxattempt
  6042. FROM {assign_submission} s
  6043. WHERE s.assignment = :assignid1 AND s.latest = 1';
  6044. $sql = 'SELECT u.id AS userid, g.grade AS grade, g.timemodified AS lastmodified,
  6045. uf.workflowstate, uf.allocatedmarker, gmx.maxattempt AS attemptnumber
  6046. FROM {user} u
  6047. LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
  6048. LEFT JOIN {assign_grades} g ON
  6049. u.id = g.userid AND
  6050. g.assignment = :assignid2 AND
  6051. g.attemptnumber = gmx.maxattempt
  6052. LEFT JOIN {assign_user_flags} uf ON uf.assignment = g.assignment AND uf.userid = g.userid
  6053. WHERE u.id ' . $userids;
  6054. $currentgrades = $DB->get_recordset_sql($sql, $params);
  6055. $modifiedusers = array();
  6056. foreach ($currentgrades as $current) {
  6057. $modified = $users[(int)$current->userid];
  6058. $grade = $this->get_user_grade($modified->userid, false);
  6059. // Check to see if the grade column was even visible.
  6060. $gradecolpresent = optional_param('quickgrade_' . $modified->userid, false, PARAM_INT) !== false;
  6061. // Check to see if the outcomes were modified.
  6062. if ($CFG->enableoutcomes) {
  6063. foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
  6064. $oldoutcome = $outcome->grades[$modified->userid]->grade;
  6065. $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
  6066. $newoutcome = optional_param($paramname, -1, PARAM_FLOAT);
  6067. // Check to see if the outcome column was even visible.
  6068. $outcomecolpresent = optional_param($paramname, false, PARAM_FLOAT) !== false;
  6069. if ($outcomecolpresent && ($oldoutcome != $newoutcome)) {
  6070. // Can't check modified time for outcomes because it is not reported.
  6071. $modifiedusers[$modified->userid] = $modified;
  6072. continue;
  6073. }
  6074. }
  6075. }
  6076. // Let plugins participate.
  6077. foreach ($this->feedbackplugins as $plugin) {
  6078. if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
  6079. // The plugins must handle is_quickgrading_modified correctly - ie
  6080. // handle hidden columns.
  6081. if ($plugin->is_quickgrading_modified($modified->userid, $grade)) {
  6082. if ((int)$current->lastmodified > (int)$modified->lastmodified) {
  6083. $message = get_string('errorrecordmodified', 'assign');
  6084. $this->set_error_message($message);
  6085. return $message;
  6086. } else {
  6087. $modifiedusers[$modified->userid] = $modified;
  6088. continue;
  6089. }
  6090. }
  6091. }
  6092. }
  6093. if (($current->grade < 0 || $current->grade === null) &&
  6094. ($modified->grade < 0 || $modified->grade === null)) {
  6095. // Different ways to indicate no grade.
  6096. $modified->grade = $current->grade; // Keep existing grade.
  6097. }
  6098. // Treat 0 and null as different values.
  6099. if ($current->grade !== null) {
  6100. $current->grade = floatval($current->grade);
  6101. }
  6102. $gradechanged = $gradecolpresent && grade_floats_different($current->grade, $modified->grade);
  6103. $markingallocationchanged = $this->get_instance()->markingworkflow &&
  6104. $this->get_instance()->markingallocation &&
  6105. ($modified->allocatedmarker !== false) &&
  6106. ($current->allocatedmarker != $modified->allocatedmarker);
  6107. $workflowstatechanged = $this->get_instance()->markingworkflow &&
  6108. ($modified->workflowstate !== false) &&
  6109. ($current->workflowstate != $modified->workflowstate);
  6110. if ($gradechanged || $markingallocationchanged || $workflowstatechanged) {
  6111. // Grade changed.
  6112. if ($this->grading_disabled($modified->userid)) {
  6113. continue;
  6114. }
  6115. $badmodified = (int)$current->lastmodified > (int)$modified->lastmodified;
  6116. $badattempt = (int)$current->attemptnumber != (int)$modified->attemptnumber;
  6117. if ($badmodified || $badattempt) {
  6118. // Error - record has been modified since viewing the page.
  6119. $message = get_string('errorrecordmodified', 'assign');
  6120. $this->set_error_message($message);
  6121. return $message;
  6122. } else {
  6123. $modifiedusers[$modified->userid] = $modified;
  6124. }
  6125. }
  6126. }
  6127. $currentgrades->close();
  6128. $adminconfig = $this->get_admin_config();
  6129. $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
  6130. // Ok - ready to process the updates.
  6131. foreach ($modifiedusers as $userid => $modified) {
  6132. $grade = $this->get_user_grade($userid, true);
  6133. $flags = $this->get_user_flags($userid, true);
  6134. $grade->grade= grade_floatval(unformat_float($modified->grade));
  6135. $grade->grader= $USER->id;
  6136. $gradecolpresent = optional_param('quickgrade_' . $userid, false, PARAM_INT) !== false;
  6137. // Save plugins data.
  6138. foreach ($this->feedbackplugins as $plugin) {
  6139. if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
  6140. $plugin->save_quickgrading_changes($userid, $grade);
  6141. if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
  6142. // This is the feedback plugin chose to push comments to the gradebook.
  6143. $grade->feedbacktext = $plugin->text_for_gradebook($grade);
  6144. $grade->feedbackformat = $plugin->format_for_gradebook($grade);
  6145. $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
  6146. }
  6147. }
  6148. }
  6149. // These will be set to false if they are not present in the quickgrading
  6150. // form (e.g. column hidden).
  6151. $workflowstatemodified = ($modified->workflowstate !== false) &&
  6152. ($flags->workflowstate != $modified->workflowstate);
  6153. $allocatedmarkermodified = ($modified->allocatedmarker !== false) &&
  6154. ($flags->allocatedmarker != $modified->allocatedmarker);
  6155. if ($workflowstatemodified) {
  6156. $flags->workflowstate = $modified->workflowstate;
  6157. }
  6158. if ($allocatedmarkermodified) {
  6159. $flags->allocatedmarker = $modified->allocatedmarker;
  6160. }
  6161. if ($workflowstatemodified || $allocatedmarkermodified) {
  6162. if ($this->update_user_flags($flags) && $workflowstatemodified) {
  6163. $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
  6164. \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $flags->workflowstate)->trigger();
  6165. }
  6166. }
  6167. $this->update_grade($grade);
  6168. // Allow teachers to skip sending notifications.
  6169. if (optional_param('sendstudentnotifications', true, PARAM_BOOL)) {
  6170. $this->notify_grade_modified($grade, true);
  6171. }
  6172. // Save outcomes.
  6173. if ($CFG->enableoutcomes) {
  6174. $data = array();
  6175. foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
  6176. $oldoutcome = $outcome->grades[$modified->userid]->grade;
  6177. $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
  6178. // This will be false if the input was not in the quickgrading
  6179. // form (e.g. column hidden).
  6180. $newoutcome = optional_param($paramname, false, PARAM_INT);
  6181. if ($newoutcome !== false && ($oldoutcome != $newoutcome)) {
  6182. $data[$outcomeid] = $newoutcome;
  6183. }
  6184. }
  6185. if (count($data) > 0) {
  6186. grade_update_outcomes('mod/assign',
  6187. $this->course->id,
  6188. 'mod',
  6189. 'assign',
  6190. $this->get_instance()->id,
  6191. $userid,
  6192. $data);
  6193. }
  6194. }
  6195. }
  6196. return get_string('quickgradingchangessaved', 'assign');
  6197. }
  6198. /**
  6199. * Reveal student identities to markers (and the gradebook).
  6200. *
  6201. * @return void
  6202. */
  6203. public function reveal_identities() {
  6204. global $DB;
  6205. require_capability('mod/assign:revealidentities', $this->context);
  6206. if ($this->get_instance()->revealidentities || empty($this->get_instance()->blindmarking)) {
  6207. return false;
  6208. }
  6209. // Update the assignment record.
  6210. $update = new stdClass();
  6211. $update->id = $this->get_instance()->id;
  6212. $update->revealidentities = 1;
  6213. $DB->update_record('assign', $update);
  6214. // Refresh the instance data.
  6215. $this->instance = null;
  6216. // Release the grades to the gradebook.
  6217. // First create the column in the gradebook.
  6218. $this->update_gradebook(false, $this->get_course_module()->id);
  6219. // Now release all grades.
  6220. $adminconfig = $this->get_admin_config();
  6221. $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
  6222. $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
  6223. $grades = $DB->get_records('assign_grades', array('assignment'=>$this->get_instance()->id));
  6224. $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
  6225. foreach ($grades as $grade) {
  6226. // Fetch any comments for this student.
  6227. if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
  6228. $grade->feedbacktext = $plugin->text_for_gradebook($grade);
  6229. $grade->feedbackformat = $plugin->format_for_gradebook($grade);
  6230. $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
  6231. }
  6232. $this->gradebook_item_update(null, $grade);
  6233. }
  6234. \mod_assign\event\identities_revealed::create_from_assign($this)->trigger();
  6235. }
  6236. /**
  6237. * Reveal student identities to markers (and the gradebook).
  6238. *
  6239. * @return void
  6240. */
  6241. protected function process_reveal_identities() {
  6242. if (!confirm_sesskey()) {
  6243. return false;
  6244. }
  6245. return $this->reveal_identities();
  6246. }
  6247. /**
  6248. * Save grading options.
  6249. *
  6250. * @return void
  6251. */
  6252. protected function process_save_grading_options() {
  6253. global $USER, $CFG;
  6254. // Include grading options form.
  6255. require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
  6256. // Need submit permission to submit an assignment.
  6257. $this->require_view_grades();
  6258. require_sesskey();
  6259. // Is advanced grading enabled?
  6260. $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
  6261. $controller = $gradingmanager->get_active_controller();
  6262. $showquickgrading = empty($controller);
  6263. if (!is_null($this->context)) {
  6264. $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
  6265. } else {
  6266. $showonlyactiveenrolopt = false;
  6267. }
  6268. $markingallocation = $this->get_instance()->markingworkflow &&
  6269. $this->get_instance()->markingallocation &&
  6270. has_capability('mod/assign:manageallocations', $this->context);
  6271. // Get markers to use in drop lists.
  6272. $markingallocationoptions = array();
  6273. if ($markingallocation) {
  6274. $markingallocationoptions[''] = get_string('filternone', 'assign');
  6275. $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
  6276. list($sort, $params) = users_order_by_sql('u');
  6277. // Only enrolled users could be assigned as potential markers.
  6278. $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
  6279. foreach ($markers as $marker) {
  6280. $markingallocationoptions[$marker->id] = fullname($marker);
  6281. }
  6282. }
  6283. // Get marking states to show in form.
  6284. $markingworkflowoptions = $this->get_marking_workflow_filters();
  6285. $gradingoptionsparams = array('cm'=>$this->get_course_module()->id,
  6286. 'contextid'=>$this->context->id,
  6287. 'userid'=>$USER->id,
  6288. 'submissionsenabled'=>$this->is_any_submission_plugin_enabled(),
  6289. 'showquickgrading'=>$showquickgrading,
  6290. 'quickgrading'=>false,
  6291. 'markingworkflowopt' => $markingworkflowoptions,
  6292. 'markingallocationopt' => $markingallocationoptions,
  6293. 'showonlyactiveenrolopt'=>$showonlyactiveenrolopt,
  6294. 'showonlyactiveenrol' => $this->show_only_active_users(),
  6295. 'downloadasfolders' => get_user_preferences('assign_downloadasfolders', 1));
  6296. $mform = new mod_assign_grading_options_form(null, $gradingoptionsparams);
  6297. if ($formdata = $mform->get_data()) {
  6298. set_user_preference('assign_perpage', $formdata->perpage);
  6299. if (isset($formdata->filter)) {
  6300. set_user_preference('assign_filter', $formdata->filter);
  6301. }
  6302. if (isset($formdata->markerfilter)) {
  6303. set_user_preference('assign_markerfilter', $formdata->markerfilter);
  6304. }
  6305. if (isset($formdata->workflowfilter)) {
  6306. set_user_preference('assign_workflowfilter', $formdata->workflowfilter);
  6307. }
  6308. if ($showquickgrading) {
  6309. set_user_preference('assign_quickgrading', isset($formdata->quickgrading));
  6310. }
  6311. if (isset($formdata->downloadasfolders)) {
  6312. set_user_preference('assign_downloadasfolders', 1); // Enabled.
  6313. } else {
  6314. set_user_preference('assign_downloadasfolders', 0); // Disabled.
  6315. }
  6316. if (!empty($showonlyactiveenrolopt)) {
  6317. $showonlyactiveenrol = isset($formdata->showonlyactiveenrol);
  6318. set_user_preference('grade_report_showonlyactiveenrol', $showonlyactiveenrol);
  6319. $this->showonlyactiveenrol = $showonlyactiveenrol;
  6320. }
  6321. }
  6322. }
  6323. /**
  6324. * Take a grade object and print a short summary for the log file.
  6325. * The size limit for the log file is 255 characters, so be careful not
  6326. * to include too much information.
  6327. *
  6328. * @deprecated since 2.7
  6329. *
  6330. * @param stdClass $grade
  6331. * @return string
  6332. */
  6333. public function format_grade_for_log(stdClass $grade) {
  6334. global $DB;
  6335. $user = $DB->get_record('user', array('id' => $grade->userid), '*', MUST_EXIST);
  6336. $info = get_string('gradestudent', 'assign', array('id'=>$user->id, 'fullname'=>fullname($user)));
  6337. if ($grade->grade != '') {
  6338. $info .= get_string('gradenoun') . ': ' . $this->display_grade($grade->grade, false) . '. ';
  6339. } else {
  6340. $info .= get_string('nograde', 'assign');
  6341. }
  6342. return $info;
  6343. }
  6344. /**
  6345. * Take a submission object and print a short summary for the log file.
  6346. * The size limit for the log file is 255 characters, so be careful not
  6347. * to include too much information.
  6348. *
  6349. * @deprecated since 2.7
  6350. *
  6351. * @param stdClass $submission
  6352. * @return string
  6353. */
  6354. public function format_submission_for_log(stdClass $submission) {
  6355. global $DB;
  6356. $info = '';
  6357. if ($submission->userid) {
  6358. $user = $DB->get_record('user', array('id' => $submission->userid), '*', MUST_EXIST);
  6359. $name = fullname($user);
  6360. } else {
  6361. $group = $this->get_submission_group($submission->userid);
  6362. if ($group) {
  6363. $name = $group->name;
  6364. } else {
  6365. $name = get_string('defaultteam', 'assign');
  6366. }
  6367. }
  6368. $status = get_string('submissionstatus_' . $submission->status, 'assign');
  6369. $params = array('id'=>$submission->userid, 'fullname'=>$name, 'status'=>$status);
  6370. $info .= get_string('submissionlog', 'assign', $params) . ' <br>';
  6371. foreach ($this->submissionplugins as $plugin) {
  6372. if ($plugin->is_enabled() && $plugin->is_visible()) {
  6373. $info .= '<br>' . $plugin->format_for_log($submission);
  6374. }
  6375. }
  6376. return $info;
  6377. }
  6378. /**
  6379. * Require a valid sess key and then call copy_previous_attempt.
  6380. *
  6381. * @param array $notices Any error messages that should be shown
  6382. * to the user at the top of the edit submission form.
  6383. * @return bool
  6384. */
  6385. protected function process_copy_previous_attempt(&$notices) {
  6386. require_sesskey();
  6387. return $this->copy_previous_attempt($notices);
  6388. }
  6389. /**
  6390. * Copy the current assignment submission from the last submitted attempt.
  6391. *
  6392. * @param array $notices Any error messages that should be shown
  6393. * to the user at the top of the edit submission form.
  6394. * @return bool
  6395. */
  6396. public function copy_previous_attempt(&$notices) {
  6397. global $USER, $CFG;
  6398. require_capability('mod/assign:submit', $this->context);
  6399. $instance = $this->get_instance();
  6400. if ($instance->teamsubmission) {
  6401. $submission = $this->get_group_submission($USER->id, 0, true);
  6402. } else {
  6403. $submission = $this->get_user_submission($USER->id, true);
  6404. }
  6405. if (!$submission || $submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
  6406. $notices[] = get_string('submissionnotcopiedinvalidstatus', 'assign');
  6407. return false;
  6408. }
  6409. $flags = $this->get_user_flags($USER->id, false);
  6410. // Get the flags to check if it is locked.
  6411. if ($flags && $flags->locked) {
  6412. $notices[] = get_string('submissionslocked', 'assign');
  6413. return false;
  6414. }
  6415. if ($instance->submissiondrafts) {
  6416. $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
  6417. } else {
  6418. $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
  6419. }
  6420. $this->update_submission($submission, $USER->id, true, $instance->teamsubmission);
  6421. // Find the previous submission.
  6422. if ($instance->teamsubmission) {
  6423. $previoussubmission = $this->get_group_submission($USER->id, 0, true, $submission->attemptnumber - 1);
  6424. } else {
  6425. $previoussubmission = $this->get_user_submission($USER->id, true, $submission->attemptnumber - 1);
  6426. }
  6427. if (!$previoussubmission) {
  6428. // There was no previous submission so there is nothing else to do.
  6429. return true;
  6430. }
  6431. $pluginerror = false;
  6432. foreach ($this->get_submission_plugins() as $plugin) {
  6433. if ($plugin->is_visible() && $plugin->is_enabled()) {
  6434. if (!$plugin->copy_submission($previoussubmission, $submission)) {
  6435. $notices[] = $plugin->get_error();
  6436. $pluginerror = true;
  6437. }
  6438. }
  6439. }
  6440. if ($pluginerror) {
  6441. return false;
  6442. }
  6443. \mod_assign\event\submission_duplicated::create_from_submission($this, $submission)->trigger();
  6444. $complete = COMPLETION_INCOMPLETE;
  6445. if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
  6446. $complete = COMPLETION_COMPLETE;
  6447. }
  6448. $completion = new completion_info($this->get_course());
  6449. if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
  6450. $this->update_activity_completion_records($instance->teamsubmission,
  6451. $instance->requireallteammemberssubmit,
  6452. $submission,
  6453. $USER->id,
  6454. $complete,
  6455. $completion);
  6456. }
  6457. if (!$instance->submissiondrafts) {
  6458. // There is a case for not notifying the student about the submission copy,
  6459. // but it provides a record of the event and if they then cancel editing it
  6460. // is clear that the submission was copied.
  6461. $this->notify_student_submission_copied($submission);
  6462. $this->notify_graders($submission);
  6463. // The same logic applies here - we could not notify teachers,
  6464. // but then they would wonder why there are submitted assignments
  6465. // and they haven't been notified.
  6466. \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
  6467. }
  6468. return true;
  6469. }
  6470. /**
  6471. * Determine if the current submission is empty or not.
  6472. *
  6473. * @param submission $submission the students submission record to check.
  6474. * @return bool
  6475. */
  6476. public function submission_empty($submission) {
  6477. $allempty = true;
  6478. foreach ($this->submissionplugins as $plugin) {
  6479. if ($plugin->is_enabled() && $plugin->is_visible()) {
  6480. if (!$allempty || !$plugin->is_empty($submission)) {
  6481. $allempty = false;
  6482. }
  6483. }
  6484. }
  6485. return $allempty;
  6486. }
  6487. /**
  6488. * Determine if a new submission is empty or not
  6489. *
  6490. * @param stdClass $data Submission data
  6491. * @return bool
  6492. */
  6493. public function new_submission_empty($data) {
  6494. foreach ($this->submissionplugins as $plugin) {
  6495. if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions() &&
  6496. !$plugin->submission_is_empty($data)) {
  6497. return false;
  6498. }
  6499. }
  6500. return true;
  6501. }
  6502. /**
  6503. * Save assignment submission for the current user.
  6504. *
  6505. * @param stdClass $data
  6506. * @param array $notices Any error messages that should be shown
  6507. * to the user.
  6508. * @return bool
  6509. */
  6510. public function save_submission(stdClass $data, & $notices) {
  6511. global $CFG, $USER, $DB;
  6512. $userid = $USER->id;
  6513. if (!empty($data->userid)) {
  6514. $userid = $data->userid;
  6515. }
  6516. $user = clone($USER);
  6517. if ($userid == $USER->id) {
  6518. require_capability('mod/assign:submit', $this->context);
  6519. } else {
  6520. $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
  6521. if (!$this->can_edit_submission($userid, $USER->id)) {
  6522. print_error('nopermission');
  6523. }
  6524. }
  6525. $instance = $this->get_instance();
  6526. if ($instance->teamsubmission) {
  6527. $submission = $this->get_group_submission($userid, 0, true);
  6528. } else {
  6529. $submission = $this->get_user_submission($userid, true);
  6530. }
  6531. if ($this->new_submission_empty($data)) {
  6532. $notices[] = get_string('submissionempty', 'mod_assign');
  6533. return false;
  6534. }
  6535. // Check that no one has modified the submission since we started looking at it.
  6536. if (isset($data->lastmodified) && ($submission->timemodified > $data->lastmodified)) {
  6537. // Another user has submitted something. Notify the current user.
  6538. if ($submission->status !== ASSIGN_SUBMISSION_STATUS_NEW) {
  6539. $notices[] = $instance->teamsubmission ? get_string('submissionmodifiedgroup', 'mod_assign')
  6540. : get_string('submissionmodified', 'mod_assign');
  6541. return false;
  6542. }
  6543. }
  6544. if ($instance->submissiondrafts) {
  6545. $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
  6546. } else {
  6547. $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
  6548. }
  6549. $flags = $this->get_user_flags($userid, false);
  6550. // Get the flags to check if it is locked.
  6551. if ($flags && $flags->locked) {
  6552. print_error('submissionslocked', 'assign');
  6553. return true;
  6554. }
  6555. $pluginerror = false;
  6556. foreach ($this->submissionplugins as $plugin) {
  6557. if ($plugin->is_enabled() && $plugin->is_visible()) {
  6558. if (!$plugin->save($submission, $data)) {
  6559. $notices[] = $plugin->get_error();
  6560. $pluginerror = true;
  6561. }
  6562. }
  6563. }
  6564. $allempty = $this->submission_empty($submission);
  6565. if ($pluginerror || $allempty) {
  6566. if ($allempty) {
  6567. $notices[] = get_string('submissionempty', 'mod_assign');
  6568. }
  6569. return false;
  6570. }
  6571. $this->update_submission($submission, $userid, true, $instance->teamsubmission);
  6572. $users = [$userid];
  6573. if ($instance->teamsubmission && !$instance->requireallteammemberssubmit) {
  6574. $team = $this->get_submission_group_members($submission->groupid, true);
  6575. foreach ($team as $member) {
  6576. if ($member->id != $userid) {
  6577. $membersubmission = clone($submission);
  6578. $this->update_submission($membersubmission, $member->id, true, $instance->teamsubmission);
  6579. $users[] = $member->id;
  6580. }
  6581. }
  6582. }
  6583. $complete = COMPLETION_INCOMPLETE;
  6584. if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
  6585. $complete = COMPLETION_COMPLETE;
  6586. }
  6587. $completion = new completion_info($this->get_course());
  6588. if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
  6589. foreach ($users as $id) {
  6590. $completion->update_state($this->get_course_module(), $complete, $id);
  6591. }
  6592. }
  6593. // Logging.
  6594. if (isset($data->submissionstatement) && ($userid == $USER->id)) {
  6595. \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
  6596. }
  6597. if (!$instance->submissiondrafts) {
  6598. $this->notify_student_submission_receipt($submission);
  6599. $this->notify_graders($submission);
  6600. \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
  6601. }
  6602. return true;
  6603. }
  6604. /**
  6605. * Save assignment submission.
  6606. *
  6607. * @param moodleform $mform
  6608. * @param array $notices Any error messages that should be shown
  6609. * to the user at the top of the edit submission form.
  6610. * @return bool
  6611. */
  6612. protected function process_save_submission(&$mform, &$notices) {
  6613. global $CFG, $USER;
  6614. // Include submission form.
  6615. require_once($CFG->dirroot . '/mod/assign/submission_form.php');
  6616. $userid = optional_param('userid', $USER->id, PARAM_INT);
  6617. // Need submit permission to submit an assignment.
  6618. require_sesskey();
  6619. if (!$this->submissions_open($userid)) {
  6620. $notices[] = get_string('duedatereached', 'assign');
  6621. return false;
  6622. }
  6623. $instance = $this->get_instance();
  6624. $data = new stdClass();
  6625. $data->userid = $userid;
  6626. $mform = new mod_assign_submission_form(null, array($this, $data));
  6627. if ($mform->is_cancelled()) {
  6628. return true;
  6629. }
  6630. if ($data = $mform->get_data()) {
  6631. return $this->save_submission($data, $notices);
  6632. }
  6633. return false;
  6634. }
  6635. /**
  6636. * Determine if this users grade can be edited.
  6637. *
  6638. * @param int $userid - The student userid
  6639. * @param bool $checkworkflow - whether to include a check for the workflow state.
  6640. * @param stdClass $gradinginfo - optional, allow gradinginfo to be passed for performance.
  6641. * @return bool $gradingdisabled
  6642. */
  6643. public function grading_disabled($userid, $checkworkflow = true, $gradinginfo = null) {
  6644. if ($checkworkflow && $this->get_instance()->markingworkflow) {
  6645. $grade = $this->get_user_grade($userid, false);
  6646. $validstates = $this->get_marking_workflow_states_for_current_user();
  6647. if (!empty($grade) && !empty($grade->workflowstate) && !array_key_exists($grade->workflowstate, $validstates)) {
  6648. return true;
  6649. }
  6650. }
  6651. if (is_null($gradinginfo)) {
  6652. $gradinginfo = grade_get_grades($this->get_course()->id,
  6653. 'mod',
  6654. 'assign',
  6655. $this->get_instance()->id,
  6656. array($userid));
  6657. }
  6658. if (!$gradinginfo) {
  6659. return false;
  6660. }
  6661. if (!isset($gradinginfo->items[0]->grades[$userid])) {
  6662. return false;
  6663. }
  6664. $gradingdisabled = $gradinginfo->items[0]->grades[$userid]->locked ||
  6665. $gradinginfo->items[0]->grades[$userid]->overridden;
  6666. return $gradingdisabled;
  6667. }
  6668. /**
  6669. * Get an instance of a grading form if advanced grading is enabled.
  6670. * This is specific to the assignment, marker and student.
  6671. *
  6672. * @param int $userid - The student userid
  6673. * @param stdClass|false $grade - The grade record
  6674. * @param bool $gradingdisabled
  6675. * @return mixed gradingform_instance|null $gradinginstance
  6676. */
  6677. protected function get_grading_instance($userid, $grade, $gradingdisabled) {
  6678. global $CFG, $USER;
  6679. $grademenu = make_grades_menu($this->get_instance()->grade);
  6680. $allowgradedecimals = $this->get_instance()->grade > 0;
  6681. $advancedgradingwarning = false;
  6682. $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
  6683. $gradinginstance = null;
  6684. if ($gradingmethod = $gradingmanager->get_active_method()) {
  6685. $controller = $gradingmanager->get_controller($gradingmethod);
  6686. if ($controller->is_form_available()) {
  6687. $itemid = null;
  6688. if ($grade) {
  6689. $itemid = $grade->id;
  6690. }
  6691. if ($gradingdisabled && $itemid) {
  6692. $gradinginstance = $controller->get_current_instance($USER->id, $itemid);
  6693. } else if (!$gradingdisabled) {
  6694. $instanceid = optional_param('advancedgradinginstanceid', 0, PARAM_INT);
  6695. $gradinginstance = $controller->get_or_create_instance($instanceid,
  6696. $USER->id,
  6697. $itemid);
  6698. }
  6699. } else {
  6700. $advancedgradingwarning = $controller->form_unavailable_notification();
  6701. }
  6702. }
  6703. if ($gradinginstance) {
  6704. $gradinginstance->get_controller()->set_grade_range($grademenu, $allowgradedecimals);
  6705. }
  6706. return $gradinginstance;
  6707. }
  6708. /**
  6709. * Add elements to grade form.
  6710. *
  6711. * @param MoodleQuickForm $mform
  6712. * @param stdClass $data
  6713. * @param array $params
  6714. * @return void
  6715. */
  6716. public function add_grade_form_elements(MoodleQuickForm $mform, stdClass $data, $params) {
  6717. global $USER, $CFG, $SESSION;
  6718. $settings = $this->get_instance();
  6719. $rownum = isset($params['rownum']) ? $params['rownum'] : 0;
  6720. $last = isset($params['last']) ? $params['last'] : true;
  6721. $useridlistid = isset($params['useridlistid']) ? $params['useridlistid'] : 0;
  6722. $userid = isset($params['userid']) ? $params['userid'] : 0;
  6723. $attemptnumber = isset($params['attemptnumber']) ? $params['attemptnumber'] : 0;
  6724. $gradingpanel = !empty($params['gradingpanel']);
  6725. $bothids = ($userid && $useridlistid);
  6726. if (!$userid || $bothids) {
  6727. $useridlist = $this->get_grading_userid_list(true, $useridlistid);
  6728. } else {
  6729. $useridlist = array($userid);
  6730. $rownum = 0;
  6731. $useridlistid = '';
  6732. }
  6733. $userid = $useridlist[$rownum];
  6734. // We need to create a grade record matching this attempt number
  6735. // or the feedback plugin will have no way to know what is the correct attempt.
  6736. $grade = $this->get_user_grade($userid, true, $attemptnumber);
  6737. $submission = null;
  6738. if ($this->get_instance()->teamsubmission) {
  6739. $submission = $this->get_group_submission($userid, 0, false, $attemptnumber);
  6740. } else {
  6741. $submission = $this->get_user_submission($userid, false, $attemptnumber);
  6742. }
  6743. // Add advanced grading.
  6744. $gradingdisabled = $this->grading_disabled($userid);
  6745. $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
  6746. $mform->addElement('header', 'gradeheader', get_string('gradenoun'));
  6747. if ($gradinginstance) {
  6748. $gradingelement = $mform->addElement('grading',
  6749. 'advancedgrading',
  6750. get_string('gradenoun') . ':',
  6751. array('gradinginstance' => $gradinginstance));
  6752. if ($gradingdisabled) {
  6753. $gradingelement->freeze();
  6754. } else {
  6755. $mform->addElement('hidden', 'advancedgradinginstanceid', $gradinginstance->get_id());
  6756. $mform->setType('advancedgradinginstanceid', PARAM_INT);
  6757. }
  6758. } else {
  6759. // Use simple direct grading.
  6760. if ($this->get_instance()->grade > 0) {
  6761. $name = get_string('gradeoutof', 'assign', $this->get_instance()->grade);
  6762. if (!$gradingdisabled) {
  6763. $gradingelement = $mform->addElement('text', 'grade', $name);
  6764. $mform->addHelpButton('grade', 'gradeoutofhelp', 'assign');
  6765. $mform->setType('grade', PARAM_RAW);
  6766. } else {
  6767. $strgradelocked = get_string('gradelocked', 'assign');
  6768. $mform->addElement('static', 'gradedisabled', $name, $strgradelocked);
  6769. $mform->addHelpButton('gradedisabled', 'gradeoutofhelp', 'assign');
  6770. }
  6771. } else {
  6772. $grademenu = array(-1 => get_string("nograde")) + make_grades_menu($this->get_instance()->grade);
  6773. if (count($grademenu) > 1) {
  6774. $gradingelement = $mform->addElement('select', 'grade', get_string('gradenoun') . ':', $grademenu);
  6775. // The grade is already formatted with format_float so it needs to be converted back to an integer.
  6776. if (!empty($data->grade)) {
  6777. $data->grade = (int)unformat_float($data->grade);
  6778. }
  6779. $mform->setType('grade', PARAM_INT);
  6780. if ($gradingdisabled) {
  6781. $gradingelement->freeze();
  6782. }
  6783. }
  6784. }
  6785. }
  6786. $gradinginfo = grade_get_grades($this->get_course()->id,
  6787. 'mod',
  6788. 'assign',
  6789. $this->get_instance()->id,
  6790. $userid);
  6791. if (!empty($CFG->enableoutcomes)) {
  6792. foreach ($gradinginfo->outcomes as $index => $outcome) {
  6793. $options = make_grades_menu(-$outcome->scaleid);
  6794. $options[0] = get_string('nooutcome', 'grades');
  6795. if ($outcome->grades[$userid]->locked) {
  6796. $mform->addElement('static',
  6797. 'outcome_' . $index . '[' . $userid . ']',
  6798. $outcome->name . ':',
  6799. $options[$outcome->grades[$userid]->grade]);
  6800. } else {
  6801. $attributes = array('id' => 'menuoutcome_' . $index );
  6802. $mform->addElement('select',
  6803. 'outcome_' . $index . '[' . $userid . ']',
  6804. $outcome->name.':',
  6805. $options,
  6806. $attributes);
  6807. $mform->setType('outcome_' . $index . '[' . $userid . ']', PARAM_INT);
  6808. $mform->setDefault('outcome_' . $index . '[' . $userid . ']',
  6809. $outcome->grades[$userid]->grade);
  6810. }
  6811. }
  6812. }
  6813. $capabilitylist = array('gradereport/grader:view', 'moodle/grade:viewall');
  6814. $usergrade = get_string('notgraded', 'assign');
  6815. if (has_all_capabilities($capabilitylist, $this->get_course_context())) {
  6816. $urlparams = array('id'=>$this->get_course()->id);
  6817. $url = new moodle_url('/grade/report/grader/index.php', $urlparams);
  6818. if (isset($gradinginfo->items[0]->grades[$userid]->grade)) {
  6819. $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
  6820. }
  6821. $gradestring = $this->get_renderer()->action_link($url, $usergrade);
  6822. } else {
  6823. if (isset($gradinginfo->items[0]->grades[$userid]) &&
  6824. !$gradinginfo->items[0]->grades[$userid]->hidden) {
  6825. $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
  6826. }
  6827. $gradestring = $usergrade;
  6828. }
  6829. if ($this->get_instance()->markingworkflow) {
  6830. $states = $this->get_marking_workflow_states_for_current_user();
  6831. $options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states;
  6832. $mform->addElement('select', 'workflowstate', get_string('markingworkflowstate', 'assign'), $options);
  6833. $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign');
  6834. $gradingstatus = $this->get_grading_status($userid);
  6835. if ($gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
  6836. if ($grade->grade && $grade->grade != -1) {
  6837. $assigngradestring = html_writer::span(
  6838. make_grades_menu($settings->grade)[grade_floatval($grade->grade)], 'currentgrade'
  6839. );
  6840. $label = get_string('currentassigngrade', 'assign');
  6841. $mform->addElement('static', 'currentassigngrade', $label, $assigngradestring);
  6842. }
  6843. }
  6844. }
  6845. if ($this->get_instance()->markingworkflow &&
  6846. $this->get_instance()->markingallocation &&
  6847. has_capability('mod/assign:manageallocations', $this->context)) {
  6848. list($sort, $params) = users_order_by_sql('u');
  6849. // Only enrolled users could be assigned as potential markers.
  6850. $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
  6851. $markerlist = array('' => get_string('choosemarker', 'assign'));
  6852. $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
  6853. foreach ($markers as $marker) {
  6854. $markerlist[$marker->id] = fullname($marker, $viewfullnames);
  6855. }
  6856. $mform->addElement('select', 'allocatedmarker', get_string('allocatedmarker', 'assign'), $markerlist);
  6857. $mform->addHelpButton('allocatedmarker', 'allocatedmarker', 'assign');
  6858. $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW);
  6859. $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW);
  6860. $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE);
  6861. $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
  6862. }
  6863. $gradestring = '<span class="currentgrade">' . $gradestring . '</span>';
  6864. $mform->addElement('static', 'currentgrade', get_string('currentgrade', 'assign'), $gradestring);
  6865. if (count($useridlist) > 1) {
  6866. $strparams = array('current'=>$rownum+1, 'total'=>count($useridlist));
  6867. $name = get_string('outof', 'assign', $strparams);
  6868. $mform->addElement('static', 'gradingstudent', get_string('gradingstudent', 'assign'), $name);
  6869. }
  6870. // Let feedback plugins add elements to the grading form.
  6871. $this->add_plugin_grade_elements($grade, $mform, $data, $userid);
  6872. // Hidden params.
  6873. $mform->addElement('hidden', 'id', $this->get_course_module()->id);
  6874. $mform->setType('id', PARAM_INT);
  6875. $mform->addElement('hidden', 'rownum', $rownum);
  6876. $mform->setType('rownum', PARAM_INT);
  6877. $mform->setConstant('rownum', $rownum);
  6878. $mform->addElement('hidden', 'useridlistid', $useridlistid);
  6879. $mform->setType('useridlistid', PARAM_ALPHANUM);
  6880. $mform->addElement('hidden', 'attemptnumber', $attemptnumber);
  6881. $mform->setType('attemptnumber', PARAM_INT);
  6882. $mform->addElement('hidden', 'ajax', optional_param('ajax', 0, PARAM_INT));
  6883. $mform->setType('ajax', PARAM_INT);
  6884. $mform->addElement('hidden', 'userid', optional_param('userid', 0, PARAM_INT));
  6885. $mform->setType('userid', PARAM_INT);
  6886. if ($this->get_instance()->teamsubmission) {
  6887. $mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign'));
  6888. $mform->addElement('selectyesno', 'applytoall', get_string('applytoteam', 'assign'));
  6889. $mform->setDefault('applytoall', 1);
  6890. }
  6891. // Do not show if we are editing a previous attempt.
  6892. if (($attemptnumber == -1 ||
  6893. ($attemptnumber + 1) == count($this->get_all_submissions($userid))) &&
  6894. $this->get_instance()->attemptreopenmethod != ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
  6895. $mform->addElement('header', 'attemptsettings', get_string('attemptsettings', 'assign'));
  6896. $attemptreopenmethod = get_string('attemptreopenmethod_' . $this->get_instance()->attemptreopenmethod, 'assign');
  6897. $mform->addElement('static', 'attemptreopenmethod', get_string('attemptreopenmethod', 'assign'), $attemptreopenmethod);
  6898. $attemptnumber = 0;
  6899. if ($submission) {
  6900. $attemptnumber = $submission->attemptnumber;
  6901. }
  6902. $maxattempts = $this->get_instance()->maxattempts;
  6903. if ($maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) {
  6904. $maxattempts = get_string('unlimitedattempts', 'assign');
  6905. }
  6906. $mform->addelement('static', 'maxattemptslabel', get_string('maxattempts', 'assign'), $maxattempts);
  6907. $mform->addelement('static', 'attemptnumberlabel', get_string('attemptnumber', 'assign'), $attemptnumber + 1);
  6908. $ismanual = $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL;
  6909. $issubmission = !empty($submission);
  6910. $isunlimited = $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS;
  6911. $islessthanmaxattempts = $issubmission && ($submission->attemptnumber < ($this->get_instance()->maxattempts-1));
  6912. if ($ismanual && (!$issubmission || $isunlimited || $islessthanmaxattempts)) {
  6913. $mform->addElement('selectyesno', 'addattempt', get_string('addattempt', 'assign'));
  6914. $mform->setDefault('addattempt', 0);
  6915. }
  6916. }
  6917. if (!$gradingpanel) {
  6918. $mform->addElement('selectyesno', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
  6919. } else {
  6920. $mform->addElement('hidden', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
  6921. $mform->setType('sendstudentnotifications', PARAM_BOOL);
  6922. }
  6923. // Get assignment visibility information for student.
  6924. $modinfo = get_fast_modinfo($settings->course, $userid);
  6925. $cm = $modinfo->get_cm($this->get_course_module()->id);
  6926. // Don't allow notification to be sent if the student can't access the assignment,
  6927. // or until in "Released" state if using marking workflow.
  6928. if (!$cm->uservisible) {
  6929. $mform->setDefault('sendstudentnotifications', 0);
  6930. $mform->freeze('sendstudentnotifications');
  6931. } else if ($this->get_instance()->markingworkflow) {
  6932. $mform->setDefault('sendstudentnotifications', 0);
  6933. if (!$gradingpanel) {
  6934. $mform->disabledIf('sendstudentnotifications', 'workflowstate', 'neq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
  6935. }
  6936. } else {
  6937. $mform->setDefault('sendstudentnotifications', $this->get_instance()->sendstudentnotifications);
  6938. }
  6939. $mform->addElement('hidden', 'action', 'submitgrade');
  6940. $mform->setType('action', PARAM_ALPHA);
  6941. if (!$gradingpanel) {
  6942. $buttonarray = array();
  6943. $name = get_string('savechanges', 'assign');
  6944. $buttonarray[] = $mform->createElement('submit', 'savegrade', $name);
  6945. if (!$last) {
  6946. $name = get_string('savenext', 'assign');
  6947. $buttonarray[] = $mform->createElement('submit', 'saveandshownext', $name);
  6948. }
  6949. $buttonarray[] = $mform->createElement('cancel', 'cancelbutton', get_string('cancel'));
  6950. $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
  6951. $mform->closeHeaderBefore('buttonar');
  6952. $buttonarray = array();
  6953. if ($rownum > 0) {
  6954. $name = get_string('previous', 'assign');
  6955. $buttonarray[] = $mform->createElement('submit', 'nosaveandprevious', $name);
  6956. }
  6957. if (!$last) {
  6958. $name = get_string('nosavebutnext', 'assign');
  6959. $buttonarray[] = $mform->createElement('submit', 'nosaveandnext', $name);
  6960. }
  6961. if (!empty($buttonarray)) {
  6962. $mform->addGroup($buttonarray, 'navar', '', array(' '), false);
  6963. }
  6964. }
  6965. // The grading form does not work well with shortforms.
  6966. $mform->setDisableShortforms();
  6967. }
  6968. /**
  6969. * Add elements in submission plugin form.
  6970. *
  6971. * @param mixed $submission stdClass|null
  6972. * @param MoodleQuickForm $mform
  6973. * @param stdClass $data
  6974. * @param int $userid The current userid (same as $USER->id)
  6975. * @return void
  6976. */
  6977. protected function add_plugin_submission_elements($submission,
  6978. MoodleQuickForm $mform,
  6979. stdClass $data,
  6980. $userid) {
  6981. foreach ($this->submissionplugins as $plugin) {
  6982. if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
  6983. $plugin->get_form_elements_for_user($submission, $mform, $data, $userid);
  6984. }
  6985. }
  6986. }
  6987. /**
  6988. * Check if feedback plugins installed are enabled.
  6989. *
  6990. * @return bool
  6991. */
  6992. public function is_any_feedback_plugin_enabled() {
  6993. if (!isset($this->cache['any_feedback_plugin_enabled'])) {
  6994. $this->cache['any_feedback_plugin_enabled'] = false;
  6995. foreach ($this->feedbackplugins as $plugin) {
  6996. if ($plugin->is_enabled() && $plugin->is_visible()) {
  6997. $this->cache['any_feedback_plugin_enabled'] = true;
  6998. break;
  6999. }
  7000. }
  7001. }
  7002. return $this->cache['any_feedback_plugin_enabled'];
  7003. }
  7004. /**
  7005. * Check if submission plugins installed are enabled.
  7006. *
  7007. * @return bool
  7008. */
  7009. public function is_any_submission_plugin_enabled() {
  7010. if (!isset($this->cache['any_submission_plugin_enabled'])) {
  7011. $this->cache['any_submission_plugin_enabled'] = false;
  7012. foreach ($this->submissionplugins as $plugin) {
  7013. if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
  7014. $this->cache['any_submission_plugin_enabled'] = true;
  7015. break;
  7016. }
  7017. }
  7018. }
  7019. return $this->cache['any_submission_plugin_enabled'];
  7020. }
  7021. /**
  7022. * Add elements to submission form.
  7023. * @param MoodleQuickForm $mform
  7024. * @param stdClass $data
  7025. * @return void
  7026. */
  7027. public function add_submission_form_elements(MoodleQuickForm $mform, stdClass $data) {
  7028. global $USER;
  7029. $userid = $data->userid;
  7030. // Team submissions.
  7031. if ($this->get_instance()->teamsubmission) {
  7032. $submission = $this->get_group_submission($userid, 0, false);
  7033. } else {
  7034. $submission = $this->get_user_submission($userid, false);
  7035. }
  7036. // Submission statement.
  7037. $adminconfig = $this->get_admin_config();
  7038. $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
  7039. $draftsenabled = $this->get_instance()->submissiondrafts;
  7040. $submissionstatement = '';
  7041. if ($requiresubmissionstatement) {
  7042. $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
  7043. }
  7044. // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
  7045. // that the submission statement checkbox will be displayed.
  7046. if (empty($submissionstatement)) {
  7047. $requiresubmissionstatement = false;
  7048. }
  7049. // Only show submission statement if we are editing our own submission.
  7050. if ($requiresubmissionstatement && !$draftsenabled && $userid == $USER->id) {
  7051. $mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement);
  7052. $mform->addRule('submissionstatement', get_string('required'), 'required', null, 'client');
  7053. }
  7054. $this->add_plugin_submission_elements($submission, $mform, $data, $userid);
  7055. // Hidden params.
  7056. $mform->addElement('hidden', 'id', $this->get_course_module()->id);
  7057. $mform->setType('id', PARAM_INT);
  7058. $mform->addElement('hidden', 'userid', $userid);
  7059. $mform->setType('userid', PARAM_INT);
  7060. $mform->addElement('hidden', 'action', 'savesubmission');
  7061. $mform->setType('action', PARAM_ALPHA);
  7062. }
  7063. /**
  7064. * Remove any data from the current submission.
  7065. *
  7066. * @param int $userid
  7067. * @return boolean
  7068. */
  7069. public function remove_submission($userid) {
  7070. global $USER;
  7071. if (!$this->can_edit_submission($userid, $USER->id)) {
  7072. $user = core_user::get_user($userid);
  7073. $message = get_string('usersubmissioncannotberemoved', 'assign', fullname($user));
  7074. $this->set_error_message($message);
  7075. return false;
  7076. }
  7077. if ($this->get_instance()->teamsubmission) {
  7078. $submission = $this->get_group_submission($userid, 0, false);
  7079. } else {
  7080. $submission = $this->get_user_submission($userid, false);
  7081. }
  7082. if (!$submission) {
  7083. return false;
  7084. }
  7085. $submission->status = $submission->attemptnumber ? ASSIGN_SUBMISSION_STATUS_REOPENED : ASSIGN_SUBMISSION_STATUS_NEW;
  7086. $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
  7087. // Tell each submission plugin we were saved with no data.
  7088. $plugins = $this->get_submission_plugins();
  7089. foreach ($plugins as $plugin) {
  7090. if ($plugin->is_enabled() && $plugin->is_visible()) {
  7091. $plugin->remove($submission);
  7092. }
  7093. }
  7094. $completion = new completion_info($this->get_course());
  7095. if ($completion->is_enabled($this->get_course_module()) &&
  7096. $this->get_instance()->completionsubmit) {
  7097. $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
  7098. }
  7099. if ($submission->userid != 0) {
  7100. \mod_assign\event\submission_status_updated::create_from_submission($this, $submission)->trigger();
  7101. }
  7102. return true;
  7103. }
  7104. /**
  7105. * Revert to draft.
  7106. *
  7107. * @param int $userid
  7108. * @return boolean
  7109. */
  7110. public function revert_to_draft($userid) {
  7111. global $DB, $USER;
  7112. // Need grade permission.
  7113. require_capability('mod/assign:grade', $this->context);
  7114. if ($this->get_instance()->teamsubmission) {
  7115. $submission = $this->get_group_submission($userid, 0, false);
  7116. } else {
  7117. $submission = $this->get_user_submission($userid, false);
  7118. }
  7119. if (!$submission) {
  7120. return false;
  7121. }
  7122. $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
  7123. $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
  7124. // Give each submission plugin a chance to process the reverting to draft.
  7125. $plugins = $this->get_submission_plugins();
  7126. foreach ($plugins as $plugin) {
  7127. if ($plugin->is_enabled() && $plugin->is_visible()) {
  7128. $plugin->revert_to_draft($submission);
  7129. }
  7130. }
  7131. // Update the modified time on the grade (grader modified).
  7132. $grade = $this->get_user_grade($userid, true);
  7133. $grade->grader = $USER->id;
  7134. $this->update_grade($grade);
  7135. $completion = new completion_info($this->get_course());
  7136. if ($completion->is_enabled($this->get_course_module()) &&
  7137. $this->get_instance()->completionsubmit) {
  7138. $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
  7139. }
  7140. \mod_assign\event\submission_status_updated::create_from_submission($this, $submission)->trigger();
  7141. return true;
  7142. }
  7143. /**
  7144. * Remove the current submission.
  7145. *
  7146. * @param int $userid
  7147. * @return boolean
  7148. */
  7149. protected function process_remove_submission($userid = 0) {
  7150. require_sesskey();
  7151. if (!$userid) {
  7152. $userid = required_param('userid', PARAM_INT);
  7153. }
  7154. return $this->remove_submission($userid);
  7155. }
  7156. /**
  7157. * Revert to draft.
  7158. * Uses url parameter userid if userid not supplied as a parameter.
  7159. *
  7160. * @param int $userid
  7161. * @return boolean
  7162. */
  7163. protected function process_revert_to_draft($userid = 0) {
  7164. require_sesskey();
  7165. if (!$userid) {
  7166. $userid = required_param('userid', PARAM_INT);
  7167. }
  7168. return $this->revert_to_draft($userid);
  7169. }
  7170. /**
  7171. * Prevent student updates to this submission
  7172. *
  7173. * @param int $userid
  7174. * @return bool
  7175. */
  7176. public function lock_submission($userid) {
  7177. global $USER, $DB;
  7178. // Need grade permission.
  7179. require_capability('mod/assign:grade', $this->context);
  7180. // Give each submission plugin a chance to process the locking.
  7181. $plugins = $this->get_submission_plugins();
  7182. $submission = $this->get_user_submission($userid, false);
  7183. $flags = $this->get_user_flags($userid, true);
  7184. $flags->locked = 1;
  7185. $this->update_user_flags($flags);
  7186. foreach ($plugins as $plugin) {
  7187. if ($plugin->is_enabled() && $plugin->is_visible()) {
  7188. $plugin->lock($submission, $flags);
  7189. }
  7190. }
  7191. $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
  7192. \mod_assign\event\submission_locked::create_from_user($this, $user)->trigger();
  7193. return true;
  7194. }
  7195. /**
  7196. * Set the workflow state for multiple users
  7197. *
  7198. * @return void
  7199. */
  7200. protected function process_set_batch_marking_workflow_state() {
  7201. global $CFG, $DB;
  7202. // Include batch marking workflow form.
  7203. require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
  7204. $formparams = array(
  7205. 'userscount' => 0, // This form is never re-displayed, so we don't need to
  7206. 'usershtml' => '', // initialise these parameters with real information.
  7207. 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
  7208. );
  7209. $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
  7210. if ($mform->is_cancelled()) {
  7211. return true;
  7212. }
  7213. if ($formdata = $mform->get_data()) {
  7214. $useridlist = explode(',', $formdata->selectedusers);
  7215. $state = $formdata->markingworkflowstate;
  7216. foreach ($useridlist as $userid) {
  7217. $flags = $this->get_user_flags($userid, true);
  7218. $flags->workflowstate = $state;
  7219. // Clear the mailed flag if notification is requested, the student hasn't been
  7220. // notified previously, the student can access the assignment, and the state
  7221. // is "Released".
  7222. $modinfo = get_fast_modinfo($this->course, $userid);
  7223. $cm = $modinfo->get_cm($this->get_course_module()->id);
  7224. if ($formdata->sendstudentnotifications && $cm->uservisible &&
  7225. $state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
  7226. $flags->mailed = 0;
  7227. }
  7228. $gradingdisabled = $this->grading_disabled($userid);
  7229. // Will not apply update if user does not have permission to assign this workflow state.
  7230. if (!$gradingdisabled && $this->update_user_flags($flags)) {
  7231. // Update Gradebook.
  7232. $grade = $this->get_user_grade($userid, true);
  7233. // Fetch any feedback for this student.
  7234. $gradebookplugin = $this->get_admin_config()->feedback_plugin_for_gradebook;
  7235. $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
  7236. $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
  7237. if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
  7238. $grade->feedbacktext = $plugin->text_for_gradebook($grade);
  7239. $grade->feedbackformat = $plugin->format_for_gradebook($grade);
  7240. $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
  7241. }
  7242. $this->update_grade($grade);
  7243. $assign = clone $this->get_instance();
  7244. $assign->cmidnumber = $this->get_course_module()->idnumber;
  7245. // Set assign gradebook feedback plugin status.
  7246. $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
  7247. $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
  7248. \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $state)->trigger();
  7249. }
  7250. }
  7251. }
  7252. }
  7253. /**
  7254. * Set the marking allocation for multiple users
  7255. *
  7256. * @return void
  7257. */
  7258. protected function process_set_batch_marking_allocation() {
  7259. global $CFG, $DB;
  7260. // Include batch marking allocation form.
  7261. require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
  7262. $formparams = array(
  7263. 'userscount' => 0, // This form is never re-displayed, so we don't need to
  7264. 'usershtml' => '' // initialise these parameters with real information.
  7265. );
  7266. list($sort, $params) = users_order_by_sql('u');
  7267. // Only enrolled users could be assigned as potential markers.
  7268. $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
  7269. $markerlist = array();
  7270. foreach ($markers as $marker) {
  7271. $markerlist[$marker->id] = fullname($marker);
  7272. }
  7273. $formparams['markers'] = $markerlist;
  7274. $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
  7275. if ($mform->is_cancelled()) {
  7276. return true;
  7277. }
  7278. if ($formdata = $mform->get_data()) {
  7279. $useridlist = explode(',', $formdata->selectedusers);
  7280. $marker = $DB->get_record('user', array('id' => $formdata->allocatedmarker), '*', MUST_EXIST);
  7281. foreach ($useridlist as $userid) {
  7282. $flags = $this->get_user_flags($userid, true);
  7283. if ($flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW ||
  7284. $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW ||
  7285. $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE ||
  7286. $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
  7287. continue; // Allocated marker can only be changed in certain workflow states.
  7288. }
  7289. $flags->allocatedmarker = $marker->id;
  7290. if ($this->update_user_flags($flags)) {
  7291. $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
  7292. \mod_assign\event\marker_updated::create_from_marker($this, $user, $marker)->trigger();
  7293. }
  7294. }
  7295. }
  7296. }
  7297. /**
  7298. * Prevent student updates to this submission.
  7299. * Uses url parameter userid.
  7300. *
  7301. * @param int $userid
  7302. * @return void
  7303. */
  7304. protected function process_lock_submission($userid = 0) {
  7305. require_sesskey();
  7306. if (!$userid) {
  7307. $userid = required_param('userid', PARAM_INT);
  7308. }
  7309. return $this->lock_submission($userid);
  7310. }
  7311. /**
  7312. * Unlock the student submission.
  7313. *
  7314. * @param int $userid
  7315. * @return bool
  7316. */
  7317. public function unlock_submission($userid) {
  7318. global $USER, $DB;
  7319. // Need grade permission.
  7320. require_capability('mod/assign:grade', $this->context);
  7321. // Give each submission plugin a chance to process the unlocking.
  7322. $plugins = $this->get_submission_plugins();
  7323. $submission = $this->get_user_submission($userid, false);
  7324. $flags = $this->get_user_flags($userid, true);
  7325. $flags->locked = 0;
  7326. $this->update_user_flags($flags);
  7327. foreach ($plugins as $plugin) {
  7328. if ($plugin->is_enabled() && $plugin->is_visible()) {
  7329. $plugin->unlock($submission, $flags);
  7330. }
  7331. }
  7332. $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
  7333. \mod_assign\event\submission_unlocked::create_from_user($this, $user)->trigger();
  7334. return true;
  7335. }
  7336. /**
  7337. * Unlock the student submission.
  7338. * Uses url parameter userid.
  7339. *
  7340. * @param int $userid
  7341. * @return bool
  7342. */
  7343. protected function process_unlock_submission($userid = 0) {
  7344. require_sesskey();
  7345. if (!$userid) {
  7346. $userid = required_param('userid', PARAM_INT);
  7347. }
  7348. return $this->unlock_submission($userid);
  7349. }
  7350. /**
  7351. * Apply a grade from a grading form to a user (may be called multiple times for a group submission).
  7352. *
  7353. * @param stdClass $formdata - the data from the form
  7354. * @param int $userid - the user to apply the grade to
  7355. * @param int $attemptnumber - The attempt number to apply the grade to.
  7356. * @return void
  7357. */
  7358. protected function apply_grade_to_user($formdata, $userid, $attemptnumber) {
  7359. global $USER, $CFG, $DB;
  7360. $grade = $this->get_user_grade($userid, true, $attemptnumber);
  7361. $originalgrade = $grade->grade;
  7362. $gradingdisabled = $this->grading_disabled($userid);
  7363. $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
  7364. if (!$gradingdisabled) {
  7365. if ($gradinginstance) {
  7366. $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading,
  7367. $grade->id);
  7368. } else {
  7369. // Handle the case when grade is set to No Grade.
  7370. if (isset($formdata->grade)) {
  7371. $grade->grade = grade_floatval(unformat_float($formdata->grade));
  7372. }
  7373. }
  7374. if (isset($formdata->workflowstate) || isset($formdata->allocatedmarker)) {
  7375. $flags = $this->get_user_flags($userid, true);
  7376. $oldworkflowstate = $flags->workflowstate;
  7377. $flags->workflowstate = isset($formdata->workflowstate) ? $formdata->workflowstate : $flags->workflowstate;
  7378. $flags->allocatedmarker = isset($formdata->allocatedmarker) ? $formdata->allocatedmarker : $flags->allocatedmarker;
  7379. if ($this->update_user_flags($flags) &&
  7380. isset($formdata->workflowstate) &&
  7381. $formdata->workflowstate !== $oldworkflowstate) {
  7382. $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
  7383. \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $formdata->workflowstate)->trigger();
  7384. }
  7385. }
  7386. }
  7387. $grade->grader= $USER->id;
  7388. $adminconfig = $this->get_admin_config();
  7389. $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
  7390. $feedbackmodified = false;
  7391. // Call save in plugins.
  7392. foreach ($this->feedbackplugins as $plugin) {
  7393. if ($plugin->is_enabled() && $plugin->is_visible()) {
  7394. $gradingmodified = $plugin->is_feedback_modified($grade, $formdata);
  7395. if ($gradingmodified) {
  7396. if (!$plugin->save($grade, $formdata)) {
  7397. $result = false;
  7398. print_error($plugin->get_error());
  7399. }
  7400. // If $feedbackmodified is true, keep it true.
  7401. $feedbackmodified = $feedbackmodified || $gradingmodified;
  7402. }
  7403. if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
  7404. // This is the feedback plugin chose to push comments to the gradebook.
  7405. $grade->feedbacktext = $plugin->text_for_gradebook($grade);
  7406. $grade->feedbackformat = $plugin->format_for_gradebook($grade);
  7407. $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
  7408. }
  7409. }
  7410. }
  7411. // We do not want to update the timemodified if no grade was added.
  7412. if (!empty($formdata->addattempt) ||
  7413. ($originalgrade !== null && $originalgrade != -1) ||
  7414. ($grade->grade !== null && $grade->grade != -1) ||
  7415. $feedbackmodified) {
  7416. $this->update_grade($grade, !empty($formdata->addattempt));
  7417. }
  7418. // We never send notifications if we have marking workflow and the grade is not released.
  7419. if ($this->get_instance()->markingworkflow &&
  7420. isset($formdata->workflowstate) &&
  7421. $formdata->workflowstate != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
  7422. $formdata->sendstudentnotifications = false;
  7423. }
  7424. // Note the default if not provided for this option is true (e.g. webservices).
  7425. // This is for backwards compatibility.
  7426. if (!isset($formdata->sendstudentnotifications) || $formdata->sendstudentnotifications) {
  7427. $this->notify_grade_modified($grade, true);
  7428. }
  7429. }
  7430. /**
  7431. * Save outcomes submitted from grading form.
  7432. *
  7433. * @param int $userid
  7434. * @param stdClass $formdata
  7435. * @param int $sourceuserid The user ID under which the outcome data is accessible. This is relevant
  7436. * for an outcome set to a user but applied to an entire group.
  7437. */
  7438. protected function process_outcomes($userid, $formdata, $sourceuserid = null) {
  7439. global $CFG, $USER;
  7440. if (empty($CFG->enableoutcomes)) {
  7441. return;
  7442. }
  7443. if ($this->grading_disabled($userid)) {
  7444. return;
  7445. }
  7446. require_once($CFG->libdir.'/gradelib.php');
  7447. $data = array();
  7448. $gradinginfo = grade_get_grades($this->get_course()->id,
  7449. 'mod',
  7450. 'assign',
  7451. $this->get_instance()->id,
  7452. $userid);
  7453. if (!empty($gradinginfo->outcomes)) {
  7454. foreach ($gradinginfo->outcomes as $index => $oldoutcome) {
  7455. $name = 'outcome_'.$index;
  7456. $sourceuserid = $sourceuserid !== null ? $sourceuserid : $userid;
  7457. if (isset($formdata->{$name}[$sourceuserid]) &&
  7458. $oldoutcome->grades[$userid]->grade != $formdata->{$name}[$sourceuserid]) {
  7459. $data[$index] = $formdata->{$name}[$sourceuserid];
  7460. }
  7461. }
  7462. }
  7463. if (count($data) > 0) {
  7464. grade_update_outcomes('mod/assign',
  7465. $this->course->id,
  7466. 'mod',
  7467. 'assign',
  7468. $this->get_instance()->id,
  7469. $userid,
  7470. $data);
  7471. }
  7472. }
  7473. /**
  7474. * If the requirements are met - reopen the submission for another attempt.
  7475. * Only call this function when grading the latest attempt.
  7476. *
  7477. * @param int $userid The userid.
  7478. * @param stdClass $submission The submission (may be a group submission).
  7479. * @param bool $addattempt - True if the "allow another attempt" checkbox was checked.
  7480. * @return bool - true if another attempt was added.
  7481. */
  7482. protected function reopen_submission_if_required($userid, $submission, $addattempt) {
  7483. $instance = $this->get_instance();
  7484. $maxattemptsreached = !empty($submission) &&
  7485. $submission->attemptnumber >= ($instance->maxattempts - 1) &&
  7486. $instance->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS;
  7487. $shouldreopen = false;
  7488. if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS) {
  7489. // Check the gradetopass from the gradebook.
  7490. $gradeitem = $this->get_grade_item();
  7491. if ($gradeitem) {
  7492. $gradegrade = grade_grade::fetch(array('userid' => $userid, 'itemid' => $gradeitem->id));
  7493. // Do not reopen if is_passed returns null, e.g. if there is no pass criterion set.
  7494. if ($gradegrade && ($gradegrade->is_passed() === false)) {
  7495. $shouldreopen = true;
  7496. }
  7497. }
  7498. }
  7499. if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL &&
  7500. !empty($addattempt)) {
  7501. $shouldreopen = true;
  7502. }
  7503. if ($shouldreopen && !$maxattemptsreached) {
  7504. $this->add_attempt($userid);
  7505. return true;
  7506. }
  7507. return false;
  7508. }
  7509. /**
  7510. * Save grade update.
  7511. *
  7512. * @param int $userid
  7513. * @param stdClass $data
  7514. * @return bool - was the grade saved
  7515. */
  7516. public function save_grade($userid, $data) {
  7517. // Need grade permission.
  7518. require_capability('mod/assign:grade', $this->context);
  7519. $instance = $this->get_instance();
  7520. $submission = null;
  7521. if ($instance->teamsubmission) {
  7522. // We need to know what the most recent group submission is.
  7523. // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
  7524. // and when deciding if we need to update the gradebook with an edited grade.
  7525. $mostrecentsubmission = $this->get_group_submission($userid, 0, false, -1);
  7526. $this->set_most_recent_team_submission($mostrecentsubmission);
  7527. // Get the submission that we are saving grades for. The data attempt number determines which submission attempt.
  7528. $submission = $this->get_group_submission($userid, 0, false, $data->attemptnumber);
  7529. } else {
  7530. $submission = $this->get_user_submission($userid, false, $data->attemptnumber);
  7531. }
  7532. if ($instance->teamsubmission && !empty($data->applytoall)) {
  7533. $groupid = 0;
  7534. if ($this->get_submission_group($userid)) {
  7535. $group = $this->get_submission_group($userid);
  7536. if ($group) {
  7537. $groupid = $group->id;
  7538. }
  7539. }
  7540. $members = $this->get_submission_group_members($groupid, true, $this->show_only_active_users());
  7541. foreach ($members as $member) {
  7542. // We only want to update the grade for this group submission attempt. The data attempt number could be
  7543. // -1 which may end up in additional attempts being created for each group member instead of just one
  7544. // additional attempt for the group.
  7545. $this->apply_grade_to_user($data, $member->id, $submission->attemptnumber);
  7546. $this->process_outcomes($member->id, $data, $userid);
  7547. }
  7548. } else {
  7549. $this->apply_grade_to_user($data, $userid, $data->attemptnumber);
  7550. $this->process_outcomes($userid, $data);
  7551. }
  7552. return true;
  7553. }
  7554. /**
  7555. * Save grade.
  7556. *
  7557. * @param moodleform $mform
  7558. * @return bool - was the grade saved
  7559. */
  7560. protected function process_save_grade(&$mform) {
  7561. global $CFG, $SESSION;
  7562. // Include grade form.
  7563. require_once($CFG->dirroot . '/mod/assign/gradeform.php');
  7564. require_sesskey();
  7565. $instance = $this->get_instance();
  7566. $rownum = required_param('rownum', PARAM_INT);
  7567. $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
  7568. $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
  7569. $userid = optional_param('userid', 0, PARAM_INT);
  7570. if (!$userid) {
  7571. if (empty($SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)])) {
  7572. // If the userid list is not stored we must not save, as it is possible that the user in a
  7573. // given row position may not be the same now as when the grading page was generated.
  7574. $url = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
  7575. throw new moodle_exception('useridlistnotcached', 'mod_assign', $url);
  7576. }
  7577. $useridlist = $SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)];
  7578. } else {
  7579. $useridlist = array($userid);
  7580. $rownum = 0;
  7581. }
  7582. $last = false;
  7583. $userid = $useridlist[$rownum];
  7584. if ($rownum == count($useridlist) - 1) {
  7585. $last = true;
  7586. }
  7587. $data = new stdClass();
  7588. $gradeformparams = array('rownum' => $rownum,
  7589. 'useridlistid' => $useridlistid,
  7590. 'last' => $last,
  7591. 'attemptnumber' => $attemptnumber,
  7592. 'userid' => $userid);
  7593. $mform = new mod_assign_grade_form(null,
  7594. array($this, $data, $gradeformparams),
  7595. 'post',
  7596. '',
  7597. array('class'=>'gradeform'));
  7598. if ($formdata = $mform->get_data()) {
  7599. return $this->save_grade($userid, $formdata);
  7600. } else {
  7601. return false;
  7602. }
  7603. }
  7604. /**
  7605. * This function is a static wrapper around can_upgrade.
  7606. *
  7607. * @param string $type The plugin type
  7608. * @param int $version The plugin version
  7609. * @return bool
  7610. */
  7611. public static function can_upgrade_assignment($type, $version) {
  7612. $assignment = new assign(null, null, null);
  7613. return $assignment->can_upgrade($type, $version);
  7614. }
  7615. /**
  7616. * This function returns true if it can upgrade an assignment from the 2.2 module.
  7617. *
  7618. * @param string $type The plugin type
  7619. * @param int $version The plugin version
  7620. * @return bool
  7621. */
  7622. public function can_upgrade($type, $version) {
  7623. if ($type == 'offline' && $version >= 2011112900) {
  7624. return true;
  7625. }
  7626. foreach ($this->submissionplugins as $plugin) {
  7627. if ($plugin->can_upgrade($type, $version)) {
  7628. return true;
  7629. }
  7630. }
  7631. foreach ($this->feedbackplugins as $plugin) {
  7632. if ($plugin->can_upgrade($type, $version)) {
  7633. return true;
  7634. }
  7635. }
  7636. return false;
  7637. }
  7638. /**
  7639. * Copy all the files from the old assignment files area to the new one.
  7640. * This is used by the plugin upgrade code.
  7641. *
  7642. * @param int $oldcontextid The old assignment context id
  7643. * @param int $oldcomponent The old assignment component ('assignment')
  7644. * @param int $oldfilearea The old assignment filearea ('submissions')
  7645. * @param int $olditemid The old submissionid (can be null e.g. intro)
  7646. * @param int $newcontextid The new assignment context id
  7647. * @param int $newcomponent The new assignment component ('assignment')
  7648. * @param int $newfilearea The new assignment filearea ('submissions')
  7649. * @param int $newitemid The new submissionid (can be null e.g. intro)
  7650. * @return int The number of files copied
  7651. */
  7652. public function copy_area_files_for_upgrade($oldcontextid,
  7653. $oldcomponent,
  7654. $oldfilearea,
  7655. $olditemid,
  7656. $newcontextid,
  7657. $newcomponent,
  7658. $newfilearea,
  7659. $newitemid) {
  7660. // Note, this code is based on some code in filestorage - but that code
  7661. // deleted the old files (which we don't want).
  7662. $count = 0;
  7663. $fs = get_file_storage();
  7664. $oldfiles = $fs->get_area_files($oldcontextid,
  7665. $oldcomponent,
  7666. $oldfilearea,
  7667. $olditemid,
  7668. 'id',
  7669. false);
  7670. foreach ($oldfiles as $oldfile) {
  7671. $filerecord = new stdClass();
  7672. $filerecord->contextid = $newcontextid;
  7673. $filerecord->component = $newcomponent;
  7674. $filerecord->filearea = $newfilearea;
  7675. $filerecord->itemid = $newitemid;
  7676. $fs->create_file_from_storedfile($filerecord, $oldfile);
  7677. $count += 1;
  7678. }
  7679. return $count;
  7680. }
  7681. /**
  7682. * Add a new attempt for each user in the list - but reopen each group assignment
  7683. * at most 1 time.
  7684. *
  7685. * @param array $useridlist Array of userids to reopen.
  7686. * @return bool
  7687. */
  7688. protected function process_add_attempt_group($useridlist) {
  7689. $groupsprocessed = array();
  7690. $result = true;
  7691. foreach ($useridlist as $userid) {
  7692. $groupid = 0;
  7693. $group = $this->get_submission_group($userid);
  7694. if ($group) {
  7695. $groupid = $group->id;
  7696. }
  7697. if (empty($groupsprocessed[$groupid])) {
  7698. // We need to know what the most recent group submission is.
  7699. // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
  7700. // and when deciding if we need to update the gradebook with an edited grade.
  7701. $currentsubmission = $this->get_group_submission($userid, 0, false, -1);
  7702. $this->set_most_recent_team_submission($currentsubmission);
  7703. $result = $this->process_add_attempt($userid) && $result;
  7704. $groupsprocessed[$groupid] = true;
  7705. }
  7706. }
  7707. return $result;
  7708. }
  7709. /**
  7710. * Check for a sess key and then call add_attempt.
  7711. *
  7712. * @param int $userid int The user to add the attempt for
  7713. * @return bool - true if successful.
  7714. */
  7715. protected function process_add_attempt($userid) {
  7716. require_sesskey();
  7717. return $this->add_attempt($userid);
  7718. }
  7719. /**
  7720. * Add a new attempt for a user.
  7721. *
  7722. * @param int $userid int The user to add the attempt for
  7723. * @return bool - true if successful.
  7724. */
  7725. protected function add_attempt($userid) {
  7726. require_capability('mod/assign:grade', $this->context);
  7727. if ($this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
  7728. return false;
  7729. }
  7730. if ($this->get_instance()->teamsubmission) {
  7731. $oldsubmission = $this->get_group_submission($userid, 0, false);
  7732. } else {
  7733. $oldsubmission = $this->get_user_submission($userid, false);
  7734. }
  7735. if (!$oldsubmission) {
  7736. return false;
  7737. }
  7738. // No more than max attempts allowed.
  7739. if ($this->get_instance()->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS &&
  7740. $oldsubmission->attemptnumber >= ($this->get_instance()->maxattempts - 1)) {
  7741. return false;
  7742. }
  7743. // Create the new submission record for the group/user.
  7744. if ($this->get_instance()->teamsubmission) {
  7745. if (isset($this->mostrecentteamsubmission)) {
  7746. // Team submissions can end up in this function for each user (via save_grade). We don't want to create
  7747. // more than one attempt for the whole team.
  7748. if ($this->mostrecentteamsubmission->attemptnumber == $oldsubmission->attemptnumber) {
  7749. $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
  7750. } else {
  7751. $newsubmission = $this->get_group_submission($userid, 0, false, $oldsubmission->attemptnumber);
  7752. }
  7753. } else {
  7754. debugging('Please use set_most_recent_team_submission() before calling add_attempt', DEBUG_DEVELOPER);
  7755. $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
  7756. }
  7757. } else {
  7758. $newsubmission = $this->get_user_submission($userid, true, $oldsubmission->attemptnumber + 1);
  7759. }
  7760. // Set the status of the new attempt to reopened.
  7761. $newsubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
  7762. // Give each submission plugin a chance to process the add_attempt.
  7763. $plugins = $this->get_submission_plugins();
  7764. foreach ($plugins as $plugin) {
  7765. if ($plugin->is_enabled() && $plugin->is_visible()) {
  7766. $plugin->add_attempt($oldsubmission, $newsubmission);
  7767. }
  7768. }
  7769. $this->update_submission($newsubmission, $userid, false, $this->get_instance()->teamsubmission);
  7770. $flags = $this->get_user_flags($userid, false);
  7771. if (isset($flags->locked) && $flags->locked) { // May not exist.
  7772. $this->process_unlock_submission($userid);
  7773. }
  7774. return true;
  7775. }
  7776. /**
  7777. * Get an upto date list of user grades and feedback for the gradebook.
  7778. *
  7779. * @param int $userid int or 0 for all users
  7780. * @return array of grade data formated for the gradebook api
  7781. * The data required by the gradebook api is userid,
  7782. * rawgrade,
  7783. * feedback,
  7784. * feedbackformat,
  7785. * usermodified,
  7786. * dategraded,
  7787. * datesubmitted
  7788. */
  7789. public function get_user_grades_for_gradebook($userid) {
  7790. global $DB, $CFG;
  7791. $grades = array();
  7792. $assignmentid = $this->get_instance()->id;
  7793. $adminconfig = $this->get_admin_config();
  7794. $gradebookpluginname = $adminconfig->feedback_plugin_for_gradebook;
  7795. $gradebookplugin = null;
  7796. // Find the gradebook plugin.
  7797. foreach ($this->feedbackplugins as $plugin) {
  7798. if ($plugin->is_enabled() && $plugin->is_visible()) {
  7799. if (('assignfeedback_' . $plugin->get_type()) == $gradebookpluginname) {
  7800. $gradebookplugin = $plugin;
  7801. }
  7802. }
  7803. }
  7804. if ($userid) {
  7805. $where = ' WHERE u.id = :userid ';
  7806. } else {
  7807. $where = ' WHERE u.id != :userid ';
  7808. }
  7809. // When the gradebook asks us for grades - only return the last attempt for each user.
  7810. $params = array('assignid1'=>$assignmentid,
  7811. 'assignid2'=>$assignmentid,
  7812. 'userid'=>$userid);
  7813. $graderesults = $DB->get_recordset_sql('SELECT
  7814. u.id as userid,
  7815. s.timemodified as datesubmitted,
  7816. g.grade as rawgrade,
  7817. g.timemodified as dategraded,
  7818. g.grader as usermodified
  7819. FROM {user} u
  7820. LEFT JOIN {assign_submission} s
  7821. ON u.id = s.userid and s.assignment = :assignid1 AND
  7822. s.latest = 1
  7823. JOIN {assign_grades} g
  7824. ON u.id = g.userid and g.assignment = :assignid2 AND
  7825. g.attemptnumber = s.attemptnumber' .
  7826. $where, $params);
  7827. foreach ($graderesults as $result) {
  7828. $gradingstatus = $this->get_grading_status($result->userid);
  7829. if (!$this->get_instance()->markingworkflow || $gradingstatus == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
  7830. $gradebookgrade = clone $result;
  7831. // Now get the feedback.
  7832. if ($gradebookplugin) {
  7833. $grade = $this->get_user_grade($result->userid, false);
  7834. if ($grade) {
  7835. $gradebookgrade->feedback = $gradebookplugin->text_for_gradebook($grade);
  7836. $gradebookgrade->feedbackformat = $gradebookplugin->format_for_gradebook($grade);
  7837. $gradebookgrade->feedbackfiles = $gradebookplugin->files_for_gradebook($grade);
  7838. }
  7839. }
  7840. $grades[$gradebookgrade->userid] = $gradebookgrade;
  7841. }
  7842. }
  7843. $graderesults->close();
  7844. return $grades;
  7845. }
  7846. /**
  7847. * Call the static version of this function
  7848. *
  7849. * @param int $userid The userid to lookup
  7850. * @return int The unique id
  7851. */
  7852. public function get_uniqueid_for_user($userid) {
  7853. return self::get_uniqueid_for_user_static($this->get_instance()->id, $userid);
  7854. }
  7855. /**
  7856. * Foreach participant in the course - assign them a random id.
  7857. *
  7858. * @param int $assignid The assignid to lookup
  7859. */
  7860. public static function allocate_unique_ids($assignid) {
  7861. global $DB;
  7862. $cm = get_coursemodule_from_instance('assign', $assignid, 0, false, MUST_EXIST);
  7863. $context = context_module::instance($cm->id);
  7864. $currentgroup = groups_get_activity_group($cm, true);
  7865. $users = get_enrolled_users($context, "mod/assign:submit", $currentgroup, 'u.id');
  7866. // Shuffle the users.
  7867. shuffle($users);
  7868. foreach ($users as $user) {
  7869. $record = $DB->get_record('assign_user_mapping',
  7870. array('assignment'=>$assignid, 'userid'=>$user->id),
  7871. 'id');
  7872. if (!$record) {
  7873. $record = new stdClass();
  7874. $record->assignment = $assignid;
  7875. $record->userid = $user->id;
  7876. $DB->insert_record('assign_user_mapping', $record);
  7877. }
  7878. }
  7879. }
  7880. /**
  7881. * Lookup this user id and return the unique id for this assignment.
  7882. *
  7883. * @param int $assignid The assignment id
  7884. * @param int $userid The userid to lookup
  7885. * @return int The unique id
  7886. */
  7887. public static function get_uniqueid_for_user_static($assignid, $userid) {
  7888. global $DB;
  7889. // Search for a record.
  7890. $params = array('assignment'=>$assignid, 'userid'=>$userid);
  7891. if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
  7892. return $record->id;
  7893. }
  7894. // Be a little smart about this - there is no record for the current user.
  7895. // We should ensure any unallocated ids for the current participant
  7896. // list are distrubited randomly.
  7897. self::allocate_unique_ids($assignid);
  7898. // Retry the search for a record.
  7899. if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
  7900. return $record->id;
  7901. }
  7902. // The requested user must not be a participant. Add a record anyway.
  7903. $record = new stdClass();
  7904. $record->assignment = $assignid;
  7905. $record->userid = $userid;
  7906. return $DB->insert_record('assign_user_mapping', $record);
  7907. }
  7908. /**
  7909. * Call the static version of this function.
  7910. *
  7911. * @param int $uniqueid The uniqueid to lookup
  7912. * @return int The user id or false if they don't exist
  7913. */
  7914. public function get_user_id_for_uniqueid($uniqueid) {
  7915. return self::get_user_id_for_uniqueid_static($this->get_instance()->id, $uniqueid);
  7916. }
  7917. /**
  7918. * Lookup this unique id and return the user id for this assignment.
  7919. *
  7920. * @param int $assignid The id of the assignment this user mapping is in
  7921. * @param int $uniqueid The uniqueid to lookup
  7922. * @return int The user id or false if they don't exist
  7923. */
  7924. public static function get_user_id_for_uniqueid_static($assignid, $uniqueid) {
  7925. global $DB;
  7926. // Search for a record.
  7927. if ($record = $DB->get_record('assign_user_mapping',
  7928. array('assignment'=>$assignid, 'id'=>$uniqueid),
  7929. 'userid',
  7930. IGNORE_MISSING)) {
  7931. return $record->userid;
  7932. }
  7933. return false;
  7934. }
  7935. /**
  7936. * Get the list of marking_workflow states the current user has permission to transition a grade to.
  7937. *
  7938. * @return array of state => description
  7939. */
  7940. public function get_marking_workflow_states_for_current_user() {
  7941. if (!empty($this->markingworkflowstates)) {
  7942. return $this->markingworkflowstates;
  7943. }
  7944. $states = array();
  7945. if (has_capability('mod/assign:grade', $this->context)) {
  7946. $states[ASSIGN_MARKING_WORKFLOW_STATE_INMARKING] = get_string('markingworkflowstateinmarking', 'assign');
  7947. $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW] = get_string('markingworkflowstatereadyforreview', 'assign');
  7948. }
  7949. if (has_any_capability(array('mod/assign:reviewgrades',
  7950. 'mod/assign:managegrades'), $this->context)) {
  7951. $states[ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW] = get_string('markingworkflowstateinreview', 'assign');
  7952. $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE] = get_string('markingworkflowstatereadyforrelease', 'assign');
  7953. }
  7954. if (has_any_capability(array('mod/assign:releasegrades',
  7955. 'mod/assign:managegrades'), $this->context)) {
  7956. $states[ASSIGN_MARKING_WORKFLOW_STATE_RELEASED] = get_string('markingworkflowstatereleased', 'assign');
  7957. }
  7958. $this->markingworkflowstates = $states;
  7959. return $this->markingworkflowstates;
  7960. }
  7961. /**
  7962. * Check is only active users in course should be shown.
  7963. *
  7964. * @return bool true if only active users should be shown.
  7965. */
  7966. public function show_only_active_users() {
  7967. global $CFG;
  7968. if (is_null($this->showonlyactiveenrol)) {
  7969. $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
  7970. $this->showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
  7971. if (!is_null($this->context)) {
  7972. $this->showonlyactiveenrol = $this->showonlyactiveenrol ||
  7973. !has_capability('moodle/course:viewsuspendedusers', $this->context);
  7974. }
  7975. }
  7976. return $this->showonlyactiveenrol;
  7977. }
  7978. /**
  7979. * Return true is user is active user in course else false
  7980. *
  7981. * @param int $userid
  7982. * @return bool true is user is active in course.
  7983. */
  7984. public function is_active_user($userid) {
  7985. return !in_array($userid, get_suspended_userids($this->context, true));
  7986. }
  7987. /**
  7988. * Returns true if gradebook feedback plugin is enabled
  7989. *
  7990. * @return bool true if gradebook feedback plugin is enabled and visible else false.
  7991. */
  7992. public function is_gradebook_feedback_enabled() {
  7993. // Get default grade book feedback plugin.
  7994. $adminconfig = $this->get_admin_config();
  7995. $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
  7996. $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
  7997. // Check if default gradebook feedback is visible and enabled.
  7998. $gradebookfeedbackplugin = $this->get_feedback_plugin_by_type($gradebookplugin);
  7999. if (empty($gradebookfeedbackplugin)) {
  8000. return false;
  8001. }
  8002. if ($gradebookfeedbackplugin->is_visible() && $gradebookfeedbackplugin->is_enabled()) {
  8003. return true;
  8004. }
  8005. // Gradebook feedback plugin is either not visible/enabled.
  8006. return false;
  8007. }
  8008. /**
  8009. * Returns the grading status.
  8010. *
  8011. * @param int $userid the user id
  8012. * @return string returns the grading status
  8013. */
  8014. public function get_grading_status($userid) {
  8015. if ($this->get_instance()->markingworkflow) {
  8016. $flags = $this->get_user_flags($userid, false);
  8017. if (!empty($flags->workflowstate)) {
  8018. return $flags->workflowstate;
  8019. }
  8020. return ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED;
  8021. } else {
  8022. $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
  8023. $grade = $this->get_user_grade($userid, false, $attemptnumber);
  8024. if (!empty($grade) && $grade->grade !== null && $grade->grade >= 0) {
  8025. return ASSIGN_GRADING_STATUS_GRADED;
  8026. } else {
  8027. return ASSIGN_GRADING_STATUS_NOT_GRADED;
  8028. }
  8029. }
  8030. }
  8031. /**
  8032. * The id used to uniquily identify the cache for this instance of the assign object.
  8033. *
  8034. * @return string
  8035. */
  8036. public function get_useridlist_key_id() {
  8037. return $this->useridlistid;
  8038. }
  8039. /**
  8040. * Generates the key that should be used for an entry in the useridlist cache.
  8041. *
  8042. * @param string $id Generate a key for this instance (optional)
  8043. * @return string The key for the id, or new entry if no $id is passed.
  8044. */
  8045. public function get_useridlist_key($id = null) {
  8046. if ($id === null) {
  8047. $id = $this->get_useridlist_key_id();
  8048. }
  8049. return $this->get_course_module()->id . '_' . $id;
  8050. }
  8051. /**
  8052. * Updates and creates the completion records in mdl_course_modules_completion.
  8053. *
  8054. * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity
  8055. * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit
  8056. * @param obj $submission the submission
  8057. * @param int $userid the user id
  8058. * @param int $complete
  8059. * @param obj $completion
  8060. *
  8061. * @return null
  8062. */
  8063. protected function update_activity_completion_records($teamsubmission,
  8064. $requireallteammemberssubmit,
  8065. $submission,
  8066. $userid,
  8067. $complete,
  8068. $completion) {
  8069. if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) ||
  8070. ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit &&
  8071. $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
  8072. $members = groups_get_members($submission->groupid);
  8073. foreach ($members as $member) {
  8074. $completion->update_state($this->get_course_module(), $complete, $member->id);
  8075. }
  8076. } else {
  8077. $completion->update_state($this->get_course_module(), $complete, $userid);
  8078. }
  8079. return;
  8080. }
  8081. /**
  8082. * Update the module completion status (set it viewed) and trigger module viewed event.
  8083. *
  8084. * @since Moodle 3.2
  8085. */
  8086. public function set_module_viewed() {
  8087. $completion = new completion_info($this->get_course());
  8088. $completion->set_module_viewed($this->get_course_module());
  8089. // Trigger the course module viewed event.
  8090. $assigninstance = $this->get_instance();
  8091. $params = [
  8092. 'objectid' => $assigninstance->id,
  8093. 'context' => $this->get_context()
  8094. ];
  8095. if ($this->is_blind_marking()) {
  8096. $params['anonymous'] = 1;
  8097. }
  8098. $event = \mod_assign\event\course_module_viewed::create($params);
  8099. $event->add_record_snapshot('assign', $assigninstance);
  8100. $event->trigger();
  8101. }
  8102. /**
  8103. * Checks for any grade notices, and adds notifications. Will display on assignment main page and grading table.
  8104. *
  8105. * @return void The notifications API will render the notifications at the appropriate part of the page.
  8106. */
  8107. protected function add_grade_notices() {
  8108. if (has_capability('mod/assign:grade', $this->get_context()) && get_config('assign', 'has_rescaled_null_grades_' . $this->get_instance()->id)) {
  8109. $link = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades'));
  8110. \core\notification::warning(get_string('fixrescalednullgrades', 'mod_assign', ['link' => $link->out()]));
  8111. }
  8112. }
  8113. /**
  8114. * View fix rescaled null grades.
  8115. *
  8116. * @return bool True if null all grades are now fixed.
  8117. */
  8118. protected function fix_null_grades() {
  8119. global $DB;
  8120. $result = $DB->set_field_select(
  8121. 'assign_grades',
  8122. 'grade',
  8123. ASSIGN_GRADE_NOT_SET,
  8124. 'grade <> ? AND grade < 0',
  8125. [ASSIGN_GRADE_NOT_SET]
  8126. );
  8127. $assign = clone $this->get_instance();
  8128. $assign->cmidnumber = $this->get_course_module()->idnumber;
  8129. assign_update_grades($assign);
  8130. return $result;
  8131. }
  8132. /**
  8133. * View fix rescaled null grades.
  8134. *
  8135. * @return void The notifications API will render the notifications at the appropriate part of the page.
  8136. */
  8137. protected function view_fix_rescaled_null_grades() {
  8138. global $OUTPUT;
  8139. $o = '';
  8140. require_capability('mod/assign:grade', $this->get_context());
  8141. $instance = $this->get_instance();
  8142. $o .= $this->get_renderer()->render(
  8143. new assign_header(
  8144. $instance,
  8145. $this->get_context(),
  8146. $this->show_intro(),
  8147. $this->get_course_module()->id
  8148. )
  8149. );
  8150. $confirm = optional_param('confirm', 0, PARAM_BOOL);
  8151. if ($confirm) {
  8152. confirm_sesskey();
  8153. // Fix the grades.
  8154. $this->fix_null_grades();
  8155. unset_config('has_rescaled_null_grades_' . $instance->id, 'assign');
  8156. // Display the notice.
  8157. $o .= $this->get_renderer()->notification(get_string('fixrescalednullgradesdone', 'assign'), 'notifysuccess');
  8158. $url = new moodle_url(
  8159. '/mod/assign/view.php',
  8160. array(
  8161. 'id' => $this->get_course_module()->id,
  8162. 'action' => 'grading'
  8163. )
  8164. );
  8165. $o .= $this->get_renderer()->continue_button($url);
  8166. } else {
  8167. // Ask for confirmation.
  8168. $continue = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades', 'confirm' => true, 'sesskey' => sesskey()));
  8169. $cancel = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
  8170. $o .= $OUTPUT->confirm(get_string('fixrescalednullgradesconfirm', 'mod_assign'), $continue, $cancel);
  8171. }
  8172. $o .= $this->view_footer();
  8173. return $o;
  8174. }
  8175. /**
  8176. * Set the most recent submission for the team.
  8177. * The most recent team submission is used to determine if another attempt should be created when allowing another
  8178. * attempt on a group assignment, and whether the gradebook should be updated.
  8179. *
  8180. * @since Moodle 3.4
  8181. * @param stdClass $submission The most recent submission of the group.
  8182. */
  8183. public function set_most_recent_team_submission($submission) {
  8184. $this->mostrecentteamsubmission = $submission;
  8185. }
  8186. /**
  8187. * Return array of valid grading allocation filters for the grading interface.
  8188. *
  8189. * @param boolean $export Export the list of filters for a template.
  8190. * @return array
  8191. */
  8192. public function get_marking_allocation_filters($export = false) {
  8193. $markingallocation = $this->get_instance()->markingworkflow &&
  8194. $this->get_instance()->markingallocation &&
  8195. has_capability('mod/assign:manageallocations', $this->context);
  8196. // Get markers to use in drop lists.
  8197. $markingallocationoptions = array();
  8198. if ($markingallocation) {
  8199. list($sort, $params) = users_order_by_sql('u');
  8200. // Only enrolled users could be assigned as potential markers.
  8201. $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
  8202. $markingallocationoptions[''] = get_string('filternone', 'assign');
  8203. $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
  8204. $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
  8205. foreach ($markers as $marker) {
  8206. $markingallocationoptions[$marker->id] = fullname($marker, $viewfullnames);
  8207. }
  8208. }
  8209. if ($export) {
  8210. $allocationfilter = get_user_preferences('assign_markerfilter', '');
  8211. $result = [];
  8212. foreach ($markingallocationoptions as $option => $label) {
  8213. array_push($result, [
  8214. 'key' => $option,
  8215. 'name' => $label,
  8216. 'active' => ($allocationfilter == $option),
  8217. ]);
  8218. }
  8219. return $result;
  8220. }
  8221. return $markingworkflowoptions;
  8222. }
  8223. /**
  8224. * Return array of valid grading workflow filters for the grading interface.
  8225. *
  8226. * @param boolean $export Export the list of filters for a template.
  8227. * @return array
  8228. */
  8229. public function get_marking_workflow_filters($export = false) {
  8230. $markingworkflow = $this->get_instance()->markingworkflow;
  8231. // Get marking states to show in form.
  8232. $markingworkflowoptions = array();
  8233. if ($markingworkflow) {
  8234. $notmarked = get_string('markingworkflowstatenotmarked', 'assign');
  8235. $markingworkflowoptions[''] = get_string('filternone', 'assign');
  8236. $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked;
  8237. $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user());
  8238. }
  8239. if ($export) {
  8240. $workflowfilter = get_user_preferences('assign_workflowfilter', '');
  8241. $result = [];
  8242. foreach ($markingworkflowoptions as $option => $label) {
  8243. array_push($result, [
  8244. 'key' => $option,
  8245. 'name' => $label,
  8246. 'active' => ($workflowfilter == $option),
  8247. ]);
  8248. }
  8249. return $result;
  8250. }
  8251. return $markingworkflowoptions;
  8252. }
  8253. /**
  8254. * Return array of valid search filters for the grading interface.
  8255. *
  8256. * @return array
  8257. */
  8258. public function get_filters() {
  8259. $filterkeys = [
  8260. ASSIGN_FILTER_NOT_SUBMITTED,
  8261. ASSIGN_FILTER_DRAFT,
  8262. ASSIGN_FILTER_SUBMITTED,
  8263. ASSIGN_FILTER_REQUIRE_GRADING,
  8264. ASSIGN_FILTER_GRANTED_EXTENSION
  8265. ];
  8266. $current = get_user_preferences('assign_filter', '');
  8267. $filters = [];
  8268. // First is always "no filter" option.
  8269. array_push($filters, [
  8270. 'key' => 'none',
  8271. 'name' => get_string('filternone', 'assign'),
  8272. 'active' => ($current == '')
  8273. ]);
  8274. foreach ($filterkeys as $key) {
  8275. array_push($filters, [
  8276. 'key' => $key,
  8277. 'name' => get_string('filter' . $key, 'assign'),
  8278. 'active' => ($current == $key)
  8279. ]);
  8280. }
  8281. return $filters;
  8282. }
  8283. /**
  8284. * Get the correct submission statement depending on single submisison, team submission or team submission
  8285. * where all team memebers must submit.
  8286. *
  8287. * @param array $adminconfig
  8288. * @param assign $instance
  8289. * @param context $context
  8290. *
  8291. * @return string
  8292. */
  8293. protected function get_submissionstatement($adminconfig, $instance, $context) {
  8294. $submissionstatement = '';
  8295. if (!($context instanceof context)) {
  8296. return $submissionstatement;
  8297. }
  8298. // Single submission.
  8299. if (!$instance->teamsubmission) {
  8300. // Single submission statement is not empty.
  8301. if (!empty($adminconfig->submissionstatement)) {
  8302. // Format the submission statement before its sent. We turn off para because this is going within
  8303. // a form element.
  8304. $options = array(
  8305. 'context' => $context,
  8306. 'para' => false
  8307. );
  8308. $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
  8309. }
  8310. } else { // Team submission.
  8311. // One user can submit for the whole team.
  8312. if (!empty($adminconfig->submissionstatementteamsubmission) && !$instance->requireallteammemberssubmit) {
  8313. // Format the submission statement before its sent. We turn off para because this is going within
  8314. // a form element.
  8315. $options = array(
  8316. 'context' => $context,
  8317. 'para' => false
  8318. );
  8319. $submissionstatement = format_text($adminconfig->submissionstatementteamsubmission,
  8320. FORMAT_MOODLE, $options);
  8321. } else if (!empty($adminconfig->submissionstatementteamsubmissionallsubmit) &&
  8322. $instance->requireallteammemberssubmit) {
  8323. // All team members must submit.
  8324. // Format the submission statement before its sent. We turn off para because this is going within
  8325. // a form element.
  8326. $options = array(
  8327. 'context' => $context,
  8328. 'para' => false
  8329. );
  8330. $submissionstatement = format_text($adminconfig->submissionstatementteamsubmissionallsubmit,
  8331. FORMAT_MOODLE, $options);
  8332. }
  8333. }
  8334. return $submissionstatement;
  8335. }
  8336. }
  8337. /**
  8338. * Portfolio caller class for mod_assign.
  8339. *
  8340. * @package mod_assign
  8341. * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
  8342. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  8343. */
  8344. class assign_portfolio_caller extends portfolio_module_caller_base {
  8345. /** @var int callback arg - the id of submission we export */
  8346. protected $sid;
  8347. /** @var string component of the submission files we export*/
  8348. protected $component;
  8349. /** @var string callback arg - the area of submission files we export */
  8350. protected $area;
  8351. /** @var int callback arg - the id of file we export */
  8352. protected $fileid;
  8353. /** @var int callback arg - the cmid of the assignment we export */
  8354. protected $cmid;
  8355. /** @var string callback arg - the plugintype of the editor we export */
  8356. protected $plugin;
  8357. /** @var string callback arg - the name of the editor field we export */
  8358. protected $editor;
  8359. /**
  8360. * Callback arg for a single file export.
  8361. */
  8362. public static function expected_callbackargs() {
  8363. return array(
  8364. 'cmid' => true,
  8365. 'sid' => false,
  8366. 'area' => false,
  8367. 'component' => false,
  8368. 'fileid' => false,
  8369. 'plugin' => false,
  8370. 'editor' => false,
  8371. );
  8372. }
  8373. /**
  8374. * The constructor.
  8375. *
  8376. * @param array $callbackargs
  8377. */
  8378. public function __construct($callbackargs) {
  8379. parent::__construct($callbackargs);
  8380. $this->cm = get_coursemodule_from_id('assign', $this->cmid, 0, false, MUST_EXIST);
  8381. }
  8382. /**
  8383. * Load data needed for the portfolio export.
  8384. *
  8385. * If the assignment type implements portfolio_load_data(), the processing is delegated
  8386. * to it. Otherwise, the caller must provide either fileid (to export single file) or
  8387. * submissionid and filearea (to export all data attached to the given submission file area)
  8388. * via callback arguments.
  8389. *
  8390. * @throws portfolio_caller_exception
  8391. */
  8392. public function load_data() {
  8393. global $DB;
  8394. $context = context_module::instance($this->cmid);
  8395. if (empty($this->fileid)) {
  8396. if (empty($this->sid) || empty($this->area)) {
  8397. throw new portfolio_caller_exception('invalidfileandsubmissionid', 'mod_assign');
  8398. }
  8399. $submission = $DB->get_record('assign_submission', array('id' => $this->sid));
  8400. } else {
  8401. $submissionid = $DB->get_field('files', 'itemid', array('id' => $this->fileid, 'contextid' => $context->id));
  8402. if ($submissionid) {
  8403. $submission = $DB->get_record('assign_submission', array('id' => $submissionid));
  8404. }
  8405. }
  8406. if (empty($submission)) {
  8407. throw new portfolio_caller_exception('filenotfound');
  8408. } else if ($submission->userid == 0) {
  8409. // This must be a group submission.
  8410. if (!groups_is_member($submission->groupid, $this->user->id)) {
  8411. throw new portfolio_caller_exception('filenotfound');
  8412. }
  8413. } else if ($this->user->id != $submission->userid) {
  8414. throw new portfolio_caller_exception('filenotfound');
  8415. }
  8416. // Export either an area of files or a single file (see function for more detail).
  8417. // The first arg is an id or null. If it is an id, the rest of the args are ignored.
  8418. // If it is null, the rest of the args are used to load a list of files from get_areafiles.
  8419. $this->set_file_and_format_data($this->fileid,
  8420. $context->id,
  8421. $this->component,
  8422. $this->area,
  8423. $this->sid,
  8424. 'timemodified',
  8425. false);
  8426. }
  8427. /**
  8428. * Prepares the package up before control is passed to the portfolio plugin.
  8429. *
  8430. * @throws portfolio_caller_exception
  8431. * @return mixed
  8432. */
  8433. public function prepare_package() {
  8434. if ($this->plugin && $this->editor) {
  8435. $options = portfolio_format_text_options();
  8436. $context = context_module::instance($this->cmid);
  8437. $options->context = $context;
  8438. $plugin = $this->get_submission_plugin();
  8439. $text = $plugin->get_editor_text($this->editor, $this->sid);
  8440. $format = $plugin->get_editor_format($this->editor, $this->sid);
  8441. $html = format_text($text, $format, $options);
  8442. $html = portfolio_rewrite_pluginfile_urls($html,
  8443. $context->id,
  8444. 'mod_assign',
  8445. $this->area,
  8446. $this->sid,
  8447. $this->exporter->get('format'));
  8448. $exporterclass = $this->exporter->get('formatclass');
  8449. if (in_array($exporterclass, array(PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_RICHHTML))) {
  8450. if ($files = $this->exporter->get('caller')->get('multifiles')) {
  8451. foreach ($files as $file) {
  8452. $this->exporter->copy_existing_file($file);
  8453. }
  8454. }
  8455. return $this->exporter->write_new_file($html, 'assignment.html', !empty($files));
  8456. } else if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
  8457. $leapwriter = $this->exporter->get('format')->leap2a_writer();
  8458. $entry = new portfolio_format_leap2a_entry($this->area . $this->cmid,
  8459. $context->get_context_name(),
  8460. 'resource',
  8461. $html);
  8462. $entry->add_category('web', 'resource_type');
  8463. $entry->author = $this->user;
  8464. $leapwriter->add_entry($entry);
  8465. if ($files = $this->exporter->get('caller')->get('multifiles')) {
  8466. $leapwriter->link_files($entry, $files, $this->area . $this->cmid . 'file');
  8467. foreach ($files as $file) {
  8468. $this->exporter->copy_existing_file($file);
  8469. }
  8470. }
  8471. return $this->exporter->write_new_file($leapwriter->to_xml(),
  8472. $this->exporter->get('format')->manifest_name(),
  8473. true);
  8474. } else {
  8475. debugging('invalid format class: ' . $this->exporter->get('formatclass'));
  8476. }
  8477. }
  8478. if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
  8479. $leapwriter = $this->exporter->get('format')->leap2a_writer();
  8480. $files = array();
  8481. if ($this->singlefile) {
  8482. $files[] = $this->singlefile;
  8483. } else if ($this->multifiles) {
  8484. $files = $this->multifiles;
  8485. } else {
  8486. throw new portfolio_caller_exception('invalidpreparepackagefile',
  8487. 'portfolio',
  8488. $this->get_return_url());
  8489. }
  8490. $entryids = array();
  8491. foreach ($files as $file) {
  8492. $entry = new portfolio_format_leap2a_file($file->get_filename(), $file);
  8493. $entry->author = $this->user;
  8494. $leapwriter->add_entry($entry);
  8495. $this->exporter->copy_existing_file($file);
  8496. $entryids[] = $entry->id;
  8497. }
  8498. if (count($files) > 1) {
  8499. $baseid = 'assign' . $this->cmid . $this->area;
  8500. $context = context_module::instance($this->cmid);
  8501. // If we have multiple files, they should be grouped together into a folder.
  8502. $entry = new portfolio_format_leap2a_entry($baseid . 'group',
  8503. $context->get_context_name(),
  8504. 'selection');
  8505. $leapwriter->add_entry($entry);
  8506. $leapwriter->make_selection($entry, $entryids, 'Folder');
  8507. }
  8508. return $this->exporter->write_new_file($leapwriter->to_xml(),
  8509. $this->exporter->get('format')->manifest_name(),
  8510. true);
  8511. }
  8512. return $this->prepare_package_file();
  8513. }
  8514. /**
  8515. * Fetch the plugin by its type.
  8516. *
  8517. * @return assign_submission_plugin
  8518. */
  8519. protected function get_submission_plugin() {
  8520. global $CFG;
  8521. if (!$this->plugin || !$this->cmid) {
  8522. return null;
  8523. }
  8524. require_once($CFG->dirroot . '/mod/assign/locallib.php');
  8525. $context = context_module::instance($this->cmid);
  8526. $assignment = new assign($context, null, null);
  8527. return $assignment->get_submission_plugin_by_type($this->plugin);
  8528. }
  8529. /**
  8530. * Calculate a sha1 has of either a single file or a list
  8531. * of files based on the data set by load_data.
  8532. *
  8533. * @return string
  8534. */
  8535. public function get_sha1() {
  8536. if ($this->plugin && $this->editor) {
  8537. $plugin = $this->get_submission_plugin();
  8538. $options = portfolio_format_text_options();
  8539. $options->context = context_module::instance($this->cmid);
  8540. $text = format_text($plugin->get_editor_text($this->editor, $this->sid),
  8541. $plugin->get_editor_format($this->editor, $this->sid),
  8542. $options);
  8543. $textsha1 = sha1($text);
  8544. $filesha1 = '';
  8545. try {
  8546. $filesha1 = $this->get_sha1_file();
  8547. } catch (portfolio_caller_exception $e) {
  8548. // No files.
  8549. }
  8550. return sha1($textsha1 . $filesha1);
  8551. }
  8552. return $this->get_sha1_file();
  8553. }
  8554. /**
  8555. * Calculate the time to transfer either a single file or a list
  8556. * of files based on the data set by load_data.
  8557. *
  8558. * @return int
  8559. */
  8560. public function expected_time() {
  8561. return $this->expected_time_file();
  8562. }
  8563. /**
  8564. * Checking the permissions.
  8565. *
  8566. * @return bool
  8567. */
  8568. public function check_permissions() {
  8569. $context = context_module::instance($this->cmid);
  8570. return has_capability('mod/assign:exportownsubmission', $context);
  8571. }
  8572. /**
  8573. * Display a module name.
  8574. *
  8575. * @return string
  8576. */
  8577. public static function display_name() {
  8578. return get_string('modulename', 'assign');
  8579. }
  8580. /**
  8581. * Return array of formats supported by this portfolio call back.
  8582. *
  8583. * @return array
  8584. */
  8585. public static function base_supported_formats() {
  8586. return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_LEAP2A);
  8587. }
  8588. }
  8589. /**
  8590. * Logic to happen when a/some group(s) has/have been deleted in a course.
  8591. *
  8592. * @param int $courseid The course ID.
  8593. * @param int $groupid The group id if it is known
  8594. * @return void
  8595. */
  8596. function assign_process_group_deleted_in_course($courseid, $groupid = null) {
  8597. global $DB;
  8598. $params = array('courseid' => $courseid);
  8599. if ($groupid) {
  8600. $params['groupid'] = $groupid;
  8601. // We just update the group that was deleted.
  8602. $sql = "SELECT o.id, o.assignid, o.groupid
  8603. FROM {assign_overrides} o
  8604. JOIN {assign} assign ON assign.id = o.assignid
  8605. WHERE assign.course = :courseid
  8606. AND o.groupid = :groupid";
  8607. } else {
  8608. // No groupid, we update all orphaned group overrides for all assign in course.
  8609. $sql = "SELECT o.id, o.assignid, o.groupid
  8610. FROM {assign_overrides} o
  8611. JOIN {assign} assign ON assign.id = o.assignid
  8612. LEFT JOIN {groups} grp ON grp.id = o.groupid
  8613. WHERE assign.course = :courseid
  8614. AND o.groupid IS NOT NULL
  8615. AND grp.id IS NULL";
  8616. }
  8617. $records = $DB->get_records_sql($sql, $params);
  8618. if (!$records) {
  8619. return; // Nothing to do.
  8620. }
  8621. $DB->delete_records_list('assign_overrides', 'id', array_keys($records));
  8622. $cache = cache::make('mod_assign', 'overrides');
  8623. foreach ($records as $record) {
  8624. $cache->delete("{$record->assignid}_g_{$record->groupid}");
  8625. }
  8626. }
  8627. /**
  8628. * Change the sort order of an override
  8629. *
  8630. * @param int $id of the override
  8631. * @param string $move direction of move
  8632. * @param int $assignid of the assignment
  8633. * @return bool success of operation
  8634. */
  8635. function move_group_override($id, $move, $assignid) {
  8636. global $DB;
  8637. // Get the override object.
  8638. if (!$override = $DB->get_record('assign_overrides', ['id' => $id], 'id, sortorder, groupid')) {
  8639. return false;
  8640. }
  8641. // Count the number of group overrides.
  8642. $overridecountgroup = $DB->count_records('assign_overrides', array('userid' => null, 'assignid' => $assignid));
  8643. // Calculate the new sortorder.
  8644. if ( ($move == 'up') and ($override->sortorder > 1)) {
  8645. $neworder = $override->sortorder - 1;
  8646. } else if (($move == 'down') and ($override->sortorder < $overridecountgroup)) {
  8647. $neworder = $override->sortorder + 1;
  8648. } else {
  8649. return false;
  8650. }
  8651. // Retrieve the override object that is currently residing in the new position.
  8652. $params = ['sortorder' => $neworder, 'assignid' => $assignid];
  8653. if ($swapoverride = $DB->get_record('assign_overrides', $params, 'id, sortorder, groupid')) {
  8654. // Swap the sortorders.
  8655. $swapoverride->sortorder = $override->sortorder;
  8656. $override->sortorder = $neworder;
  8657. // Update the override records.
  8658. $DB->update_record('assign_overrides', $override);
  8659. $DB->update_record('assign_overrides', $swapoverride);
  8660. // Delete cache for the 2 records we updated above.
  8661. $cache = cache::make('mod_assign', 'overrides');
  8662. $cache->delete("{$override->assignid}_g_{$override->groupid}");
  8663. $cache->delete("{$swapoverride->assignid}_g_{$swapoverride->groupid}");
  8664. }
  8665. reorder_group_overrides($assignid);
  8666. return true;
  8667. }
  8668. /**
  8669. * Reorder the overrides starting at the override at the given startorder.
  8670. *
  8671. * @param int $assignid of the assigment
  8672. */
  8673. function reorder_group_overrides($assignid) {
  8674. global $DB;
  8675. $i = 1;
  8676. if ($overrides = $DB->get_records('assign_overrides', array('userid' => null, 'assignid' => $assignid), 'sortorder ASC')) {
  8677. $cache = cache::make('mod_assign', 'overrides');
  8678. foreach ($overrides as $override) {
  8679. $f = new stdClass();
  8680. $f->id = $override->id;
  8681. $f->sortorder = $i++;
  8682. $DB->update_record('assign_overrides', $f);
  8683. $cache->delete("{$assignid}_g_{$override->groupid}");
  8684. // Update priorities of group overrides.
  8685. $params = [
  8686. 'modulename' => 'assign',
  8687. 'instance' => $override->assignid,
  8688. 'groupid' => $override->groupid
  8689. ];
  8690. $DB->set_field('event', 'priority', $f->sortorder, $params);
  8691. }
  8692. }
  8693. }