PageRenderTime 292ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 3ms

/mod/assign/locallib.php

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