MDL-66203 mod_assign: Reset submission status when removing a submission
[moodle.git] / mod / assign / locallib.php
blob9a827cf64f8e8c36cef5e02092b72ccafb0bb68d
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.
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/>.
17 /**
18 * This file contains the definition for the class assignment
20 * This class provides all the functionality for the new assign module.
22 * @package mod_assign
23 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
29 // Assignment submission statuses.
30 define('ASSIGN_SUBMISSION_STATUS_NEW', 'new');
31 define('ASSIGN_SUBMISSION_STATUS_REOPENED', 'reopened');
32 define('ASSIGN_SUBMISSION_STATUS_DRAFT', 'draft');
33 define('ASSIGN_SUBMISSION_STATUS_SUBMITTED', 'submitted');
35 // Search filters for grading page.
36 define('ASSIGN_FILTER_NONE', 'none');
37 define('ASSIGN_FILTER_SUBMITTED', 'submitted');
38 define('ASSIGN_FILTER_NOT_SUBMITTED', 'notsubmitted');
39 define('ASSIGN_FILTER_SINGLE_USER', 'singleuser');
40 define('ASSIGN_FILTER_REQUIRE_GRADING', 'requiregrading');
41 define('ASSIGN_FILTER_GRANTED_EXTENSION', 'grantedextension');
42 define('ASSIGN_FILTER_DRAFT', 'draft');
44 // Marker filter for grading page.
45 define('ASSIGN_MARKER_FILTER_NO_MARKER', -1);
47 // Reopen attempt methods.
48 define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none');
49 define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual');
50 define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass');
52 // Special value means allow unlimited attempts.
53 define('ASSIGN_UNLIMITED_ATTEMPTS', -1);
55 // Special value means no grade has been set.
56 define('ASSIGN_GRADE_NOT_SET', -1);
58 // Grading states.
59 define('ASSIGN_GRADING_STATUS_GRADED', 'graded');
60 define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded');
62 // Marking workflow states.
63 define('ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED', 'notmarked');
64 define('ASSIGN_MARKING_WORKFLOW_STATE_INMARKING', 'inmarking');
65 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW', 'readyforreview');
66 define('ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW', 'inreview');
67 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE', 'readyforrelease');
68 define('ASSIGN_MARKING_WORKFLOW_STATE_RELEASED', 'released');
70 /** ASSIGN_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
71 define("ASSIGN_MAX_EVENT_LENGTH", "432000");
73 // Name of file area for intro attachments.
74 define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment');
76 // Event types.
77 define('ASSIGN_EVENT_TYPE_DUE', 'due');
78 define('ASSIGN_EVENT_TYPE_GRADINGDUE', 'gradingdue');
79 define('ASSIGN_EVENT_TYPE_OPEN', 'open');
80 define('ASSIGN_EVENT_TYPE_CLOSE', 'close');
82 require_once($CFG->libdir . '/accesslib.php');
83 require_once($CFG->libdir . '/formslib.php');
84 require_once($CFG->dirroot . '/repository/lib.php');
85 require_once($CFG->dirroot . '/mod/assign/mod_form.php');
86 require_once($CFG->libdir . '/gradelib.php');
87 require_once($CFG->dirroot . '/grade/grading/lib.php');
88 require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php');
89 require_once($CFG->dirroot . '/mod/assign/submissionplugin.php');
90 require_once($CFG->dirroot . '/mod/assign/renderable.php');
91 require_once($CFG->dirroot . '/mod/assign/gradingtable.php');
92 require_once($CFG->libdir . '/portfolio/caller.php');
94 use \mod_assign\output\grading_app;
96 /**
97 * Standard base class for mod_assign (assignment types).
99 * @package mod_assign
100 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
101 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
103 class assign {
105 /** @var stdClass the assignment record that contains the global settings for this assign instance */
106 private $instance;
108 /** @var array $var array an array containing per-user assignment records, each having calculated properties (e.g. dates) */
109 private $userinstances = [];
111 /** @var grade_item the grade_item record for this assign instance's primary grade item. */
112 private $gradeitem;
114 /** @var context the context of the course module for this assign instance
115 * (or just the course if we are creating a new one)
117 private $context;
119 /** @var stdClass the course this assign instance belongs to */
120 private $course;
122 /** @var stdClass the admin config for all assign instances */
123 private $adminconfig;
125 /** @var assign_renderer the custom renderer for this module */
126 private $output;
128 /** @var cm_info the course module for this assign instance */
129 private $coursemodule;
131 /** @var array cache for things like the coursemodule name or the scale menu -
132 * only lives for a single request.
134 private $cache;
136 /** @var array list of the installed submission plugins */
137 private $submissionplugins;
139 /** @var array list of the installed feedback plugins */
140 private $feedbackplugins;
142 /** @var string action to be used to return to this page
143 * (without repeating any form submissions etc).
145 private $returnaction = 'view';
147 /** @var array params to be used to return to this page */
148 private $returnparams = array();
150 /** @var string modulename prevents excessive calls to get_string */
151 private static $modulename = null;
153 /** @var string modulenameplural prevents excessive calls to get_string */
154 private static $modulenameplural = null;
156 /** @var array of marking workflow states for the current user */
157 private $markingworkflowstates = null;
159 /** @var bool whether to exclude users with inactive enrolment */
160 private $showonlyactiveenrol = null;
162 /** @var string A key used to identify userlists created by this object. */
163 private $useridlistid = null;
165 /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */
166 private $participants = array();
168 /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */
169 private $usersubmissiongroups = array();
171 /** @var array cached list of user groups. The cache key will be the user. */
172 private $usergroups = array();
174 /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */
175 private $sharedgroupmembers = array();
178 * @var stdClass The most recent team submission. Used to determine additional attempt numbers and whether
179 * to update the gradebook.
181 private $mostrecentteamsubmission = null;
183 /** @var array Array of error messages encountered during the execution of assignment related operations. */
184 private $errors = array();
187 * Constructor for the base assign class.
189 * Note: For $coursemodule you can supply a stdclass if you like, but it
190 * will be more efficient to supply a cm_info object.
192 * @param mixed $coursemodulecontext context|null the course module context
193 * (or the course context if the coursemodule has not been
194 * created yet).
195 * @param mixed $coursemodule the current course module if it was already loaded,
196 * otherwise this class will load one from the context as required.
197 * @param mixed $course the current course if it was already loaded,
198 * otherwise this class will load one from the context as required.
200 public function __construct($coursemodulecontext, $coursemodule, $course) {
201 global $SESSION;
203 $this->context = $coursemodulecontext;
204 $this->course = $course;
206 // Ensure that $this->coursemodule is a cm_info object (or null).
207 $this->coursemodule = cm_info::create($coursemodule);
209 // Temporary cache only lives for a single request - used to reduce db lookups.
210 $this->cache = array();
212 $this->submissionplugins = $this->load_plugins('assignsubmission');
213 $this->feedbackplugins = $this->load_plugins('assignfeedback');
215 // Extra entropy is required for uniqid() to work on cygwin.
216 $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM);
218 if (!isset($SESSION->mod_assign_useridlist)) {
219 $SESSION->mod_assign_useridlist = [];
224 * Set the action and parameters that can be used to return to the current page.
226 * @param string $action The action for the current page
227 * @param array $params An array of name value pairs which form the parameters
228 * to return to the current page.
229 * @return void
231 public function register_return_link($action, $params) {
232 global $PAGE;
233 $params['action'] = $action;
234 $cm = $this->get_course_module();
235 if ($cm) {
236 $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $cm->id));
237 } else {
238 $currenturl = new moodle_url('/mod/assign/index.php', array('id' => $this->get_course()->id));
241 $currenturl->params($params);
242 $PAGE->set_url($currenturl);
246 * Return an action that can be used to get back to the current page.
248 * @return string action
250 public function get_return_action() {
251 global $PAGE;
253 // Web services don't set a URL, we should avoid debugging when ussing the url object.
254 if (!WS_SERVER) {
255 $params = $PAGE->url->params();
258 if (!empty($params['action'])) {
259 return $params['action'];
261 return '';
265 * Based on the current assignment settings should we display the intro.
267 * @return bool showintro
269 public function show_intro() {
270 if ($this->get_instance()->alwaysshowdescription ||
271 time() > $this->get_instance()->allowsubmissionsfromdate) {
272 return true;
274 return false;
278 * Return a list of parameters that can be used to get back to the current page.
280 * @return array params
282 public function get_return_params() {
283 global $PAGE;
285 $params = array();
286 if (!WS_SERVER) {
287 $params = $PAGE->url->params();
289 unset($params['id']);
290 unset($params['action']);
291 return $params;
295 * Set the submitted form data.
297 * @param stdClass $data The form data (instance)
299 public function set_instance(stdClass $data) {
300 $this->instance = $data;
304 * Set the context.
306 * @param context $context The new context
308 public function set_context(context $context) {
309 $this->context = $context;
313 * Set the course data.
315 * @param stdClass $course The course data
317 public function set_course(stdClass $course) {
318 $this->course = $course;
322 * Set error message.
324 * @param string $message The error message
326 protected function set_error_message(string $message) {
327 $this->errors[] = $message;
331 * Get error messages.
333 * @return array The array of error messages
335 protected function get_error_messages(): array {
336 return $this->errors;
340 * Get list of feedback plugins installed.
342 * @return array
344 public function get_feedback_plugins() {
345 return $this->feedbackplugins;
349 * Get list of submission plugins installed.
351 * @return array
353 public function get_submission_plugins() {
354 return $this->submissionplugins;
358 * Is blind marking enabled and reveal identities not set yet?
360 * @return bool
362 public function is_blind_marking() {
363 return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities;
367 * Is hidden grading enabled?
369 * This just checks the assignment settings. Remember to check
370 * the user has the 'showhiddengrader' capability too
372 * @return bool
374 public function is_hidden_grader() {
375 return $this->get_instance()->hidegrader;
379 * Does an assignment have submission(s) or grade(s) already?
381 * @return bool
383 public function has_submissions_or_grades() {
384 $allgrades = $this->count_grades();
385 $allsubmissions = $this->count_submissions();
386 if (($allgrades == 0) && ($allsubmissions == 0)) {
387 return false;
389 return true;
393 * Get a specific submission plugin by its type.
395 * @param string $subtype assignsubmission | assignfeedback
396 * @param string $type
397 * @return mixed assign_plugin|null
399 public function get_plugin_by_type($subtype, $type) {
400 $shortsubtype = substr($subtype, strlen('assign'));
401 $name = $shortsubtype . 'plugins';
402 if ($name != 'feedbackplugins' && $name != 'submissionplugins') {
403 return null;
405 $pluginlist = $this->$name;
406 foreach ($pluginlist as $plugin) {
407 if ($plugin->get_type() == $type) {
408 return $plugin;
411 return null;
415 * Get a feedback plugin by type.
417 * @param string $type - The type of plugin e.g comments
418 * @return mixed assign_feedback_plugin|null
420 public function get_feedback_plugin_by_type($type) {
421 return $this->get_plugin_by_type('assignfeedback', $type);
425 * Get a submission plugin by type.
427 * @param string $type - The type of plugin e.g comments
428 * @return mixed assign_submission_plugin|null
430 public function get_submission_plugin_by_type($type) {
431 return $this->get_plugin_by_type('assignsubmission', $type);
435 * Load the plugins from the sub folders under subtype.
437 * @param string $subtype - either submission or feedback
438 * @return array - The sorted list of plugins
440 public function load_plugins($subtype) {
441 global $CFG;
442 $result = array();
444 $names = core_component::get_plugin_list($subtype);
446 foreach ($names as $name => $path) {
447 if (file_exists($path . '/locallib.php')) {
448 require_once($path . '/locallib.php');
450 $shortsubtype = substr($subtype, strlen('assign'));
451 $pluginclass = 'assign_' . $shortsubtype . '_' . $name;
453 $plugin = new $pluginclass($this, $name);
455 if ($plugin instanceof assign_plugin) {
456 $idx = $plugin->get_sort_order();
457 while (array_key_exists($idx, $result)) {
458 $idx +=1;
460 $result[$idx] = $plugin;
464 ksort($result);
465 return $result;
469 * Display the assignment, used by view.php
471 * The assignment is displayed differently depending on your role,
472 * the settings for the assignment and the status of the assignment.
474 * @param string $action The current action if any.
475 * @param array $args Optional arguments to pass to the view (instead of getting them from GET and POST).
476 * @return string - The page output.
478 public function view($action='', $args = array()) {
479 global $PAGE;
481 $o = '';
482 $mform = null;
483 $notices = array();
484 $nextpageparams = array();
486 if (!empty($this->get_course_module()->id)) {
487 $nextpageparams['id'] = $this->get_course_module()->id;
490 // Handle form submissions first.
491 if ($action == 'savesubmission') {
492 $action = 'editsubmission';
493 if ($this->process_save_submission($mform, $notices)) {
494 $action = 'redirect';
495 if ($this->can_grade()) {
496 $nextpageparams['action'] = 'grading';
497 } else {
498 $nextpageparams['action'] = 'view';
501 } else if ($action == 'editprevioussubmission') {
502 $action = 'editsubmission';
503 if ($this->process_copy_previous_attempt($notices)) {
504 $action = 'redirect';
505 $nextpageparams['action'] = 'editsubmission';
507 } else if ($action == 'lock') {
508 $this->process_lock_submission();
509 $action = 'redirect';
510 $nextpageparams['action'] = 'grading';
511 } else if ($action == 'removesubmission') {
512 $this->process_remove_submission();
513 $action = 'redirect';
514 if ($this->can_grade()) {
515 $nextpageparams['action'] = 'grading';
516 } else {
517 $nextpageparams['action'] = 'view';
519 } else if ($action == 'addattempt') {
520 $this->process_add_attempt(required_param('userid', PARAM_INT));
521 $action = 'redirect';
522 $nextpageparams['action'] = 'grading';
523 } else if ($action == 'reverttodraft') {
524 $this->process_revert_to_draft();
525 $action = 'redirect';
526 $nextpageparams['action'] = 'grading';
527 } else if ($action == 'unlock') {
528 $this->process_unlock_submission();
529 $action = 'redirect';
530 $nextpageparams['action'] = 'grading';
531 } else if ($action == 'setbatchmarkingworkflowstate') {
532 $this->process_set_batch_marking_workflow_state();
533 $action = 'redirect';
534 $nextpageparams['action'] = 'grading';
535 } else if ($action == 'setbatchmarkingallocation') {
536 $this->process_set_batch_marking_allocation();
537 $action = 'redirect';
538 $nextpageparams['action'] = 'grading';
539 } else if ($action == 'confirmsubmit') {
540 $action = 'submit';
541 if ($this->process_submit_for_grading($mform, $notices)) {
542 $action = 'redirect';
543 $nextpageparams['action'] = 'view';
544 } else if ($notices) {
545 $action = 'viewsubmitforgradingerror';
547 } else if ($action == 'submitotherforgrading') {
548 if ($this->process_submit_other_for_grading($mform, $notices)) {
549 $action = 'redirect';
550 $nextpageparams['action'] = 'grading';
551 } else {
552 $action = 'viewsubmitforgradingerror';
554 } else if ($action == 'gradingbatchoperation') {
555 $action = $this->process_grading_batch_operation($mform);
556 if ($action == 'grading') {
557 $action = 'redirect';
558 $nextpageparams['action'] = 'grading';
560 } else if ($action == 'submitgrade') {
561 if (optional_param('saveandshownext', null, PARAM_RAW)) {
562 // Save and show next.
563 $action = 'grade';
564 if ($this->process_save_grade($mform)) {
565 $action = 'redirect';
566 $nextpageparams['action'] = 'grade';
567 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
568 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
570 } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) {
571 $action = 'redirect';
572 $nextpageparams['action'] = 'grade';
573 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1;
574 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
575 } else if (optional_param('nosaveandnext', null, PARAM_RAW)) {
576 $action = 'redirect';
577 $nextpageparams['action'] = 'grade';
578 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
579 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
580 } else if (optional_param('savegrade', null, PARAM_RAW)) {
581 // Save changes button.
582 $action = 'grade';
583 if ($this->process_save_grade($mform)) {
584 $action = 'redirect';
585 $nextpageparams['action'] = 'savegradingresult';
587 } else {
588 // Cancel button.
589 $action = 'redirect';
590 $nextpageparams['action'] = 'grading';
592 } else if ($action == 'quickgrade') {
593 $message = $this->process_save_quick_grades();
594 $action = 'quickgradingresult';
595 } else if ($action == 'saveoptions') {
596 $this->process_save_grading_options();
597 $action = 'redirect';
598 $nextpageparams['action'] = 'grading';
599 } else if ($action == 'saveextension') {
600 $action = 'grantextension';
601 if ($this->process_save_extension($mform)) {
602 $action = 'redirect';
603 $nextpageparams['action'] = 'grading';
605 } else if ($action == 'revealidentitiesconfirm') {
606 $this->process_reveal_identities();
607 $action = 'redirect';
608 $nextpageparams['action'] = 'grading';
611 $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT),
612 'useridlistid' => optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM));
613 $this->register_return_link($action, $returnparams);
615 // Include any page action as part of the body tag CSS id.
616 if (!empty($action)) {
617 $PAGE->set_pagetype('mod-assign-' . $action);
619 // Now show the right view page.
620 if ($action == 'redirect') {
621 $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams);
622 $messages = '';
623 $messagetype = \core\output\notification::NOTIFY_INFO;
624 $errors = $this->get_error_messages();
625 if (!empty($errors)) {
626 $messages = html_writer::alist($errors, ['class' => 'mb-1 mt-1']);
627 $messagetype = \core\output\notification::NOTIFY_ERROR;
629 redirect($nextpageurl, $messages, null, $messagetype);
630 return;
631 } else if ($action == 'savegradingresult') {
632 $message = get_string('gradingchangessaved', 'assign');
633 $o .= $this->view_savegrading_result($message);
634 } else if ($action == 'quickgradingresult') {
635 $mform = null;
636 $o .= $this->view_quickgrading_result($message);
637 } else if ($action == 'gradingpanel') {
638 $o .= $this->view_single_grading_panel($args);
639 } else if ($action == 'grade') {
640 $o .= $this->view_single_grade_page($mform);
641 } else if ($action == 'viewpluginassignfeedback') {
642 $o .= $this->view_plugin_content('assignfeedback');
643 } else if ($action == 'viewpluginassignsubmission') {
644 $o .= $this->view_plugin_content('assignsubmission');
645 } else if ($action == 'editsubmission') {
646 $o .= $this->view_edit_submission_page($mform, $notices);
647 } else if ($action == 'grader') {
648 $o .= $this->view_grader();
649 } else if ($action == 'grading') {
650 $o .= $this->view_grading_page();
651 } else if ($action == 'downloadall') {
652 $o .= $this->download_submissions();
653 } else if ($action == 'submit') {
654 $o .= $this->check_submit_for_grading($mform);
655 } else if ($action == 'grantextension') {
656 $o .= $this->view_grant_extension($mform);
657 } else if ($action == 'revealidentities') {
658 $o .= $this->view_reveal_identities_confirm($mform);
659 } else if ($action == 'removesubmissionconfirm') {
660 $o .= $this->view_remove_submission_confirm();
661 } else if ($action == 'plugingradingbatchoperation') {
662 $o .= $this->view_plugin_grading_batch_operation($mform);
663 } else if ($action == 'viewpluginpage') {
664 $o .= $this->view_plugin_page();
665 } else if ($action == 'viewcourseindex') {
666 $o .= $this->view_course_index();
667 } else if ($action == 'viewbatchsetmarkingworkflowstate') {
668 $o .= $this->view_batch_set_workflow_state($mform);
669 } else if ($action == 'viewbatchmarkingallocation') {
670 $o .= $this->view_batch_markingallocation($mform);
671 } else if ($action == 'viewsubmitforgradingerror') {
672 $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices);
673 } else if ($action == 'fixrescalednullgrades') {
674 $o .= $this->view_fix_rescaled_null_grades();
675 } else {
676 $o .= $this->view_submission_page();
679 return $o;
683 * Add this instance to the database.
685 * @param stdClass $formdata The data submitted from the form
686 * @param bool $callplugins This is used to skip the plugin code
687 * when upgrading an old assignment to a new one (the plugins get called manually)
688 * @return mixed false if an error occurs or the int id of the new instance
690 public function add_instance(stdClass $formdata, $callplugins) {
691 global $DB;
692 $adminconfig = $this->get_admin_config();
694 $err = '';
696 // Add the database record.
697 $update = new stdClass();
698 $update->name = $formdata->name;
699 $update->timemodified = time();
700 $update->timecreated = time();
701 $update->course = $formdata->course;
702 $update->courseid = $formdata->course;
703 $update->intro = $formdata->intro;
704 $update->introformat = $formdata->introformat;
705 $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
706 $update->submissiondrafts = $formdata->submissiondrafts;
707 $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
708 $update->sendnotifications = $formdata->sendnotifications;
709 $update->sendlatenotifications = $formdata->sendlatenotifications;
710 $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
711 if (isset($formdata->sendstudentnotifications)) {
712 $update->sendstudentnotifications = $formdata->sendstudentnotifications;
714 $update->duedate = $formdata->duedate;
715 $update->cutoffdate = $formdata->cutoffdate;
716 $update->gradingduedate = $formdata->gradingduedate;
717 $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
718 $update->grade = $formdata->grade;
719 $update->completionsubmit = !empty($formdata->completionsubmit);
720 $update->teamsubmission = $formdata->teamsubmission;
721 $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
722 if (isset($formdata->teamsubmissiongroupingid)) {
723 $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
725 $update->blindmarking = $formdata->blindmarking;
726 if (isset($formdata->hidegrader)) {
727 $update->hidegrader = $formdata->hidegrader;
729 $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
730 if (!empty($formdata->attemptreopenmethod)) {
731 $update->attemptreopenmethod = $formdata->attemptreopenmethod;
733 if (!empty($formdata->maxattempts)) {
734 $update->maxattempts = $formdata->maxattempts;
736 if (isset($formdata->preventsubmissionnotingroup)) {
737 $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
739 $update->markingworkflow = $formdata->markingworkflow;
740 $update->markingallocation = $formdata->markingallocation;
741 if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
742 $update->markingallocation = 0;
745 $returnid = $DB->insert_record('assign', $update);
746 $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
747 // Cache the course record.
748 $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST);
750 $this->save_intro_draft_files($formdata);
752 if ($callplugins) {
753 // Call save_settings hook for submission plugins.
754 foreach ($this->submissionplugins as $plugin) {
755 if (!$this->update_plugin_instance($plugin, $formdata)) {
756 print_error($plugin->get_error());
757 return false;
760 foreach ($this->feedbackplugins as $plugin) {
761 if (!$this->update_plugin_instance($plugin, $formdata)) {
762 print_error($plugin->get_error());
763 return false;
767 // In the case of upgrades the coursemodule has not been set,
768 // so we need to wait before calling these two.
769 $this->update_calendar($formdata->coursemodule);
770 if (!empty($formdata->completionexpected)) {
771 \core_completion\api::update_completion_date_event($formdata->coursemodule, 'assign', $this->instance,
772 $formdata->completionexpected);
774 $this->update_gradebook(false, $formdata->coursemodule);
778 $update = new stdClass();
779 $update->id = $this->get_instance()->id;
780 $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
781 $DB->update_record('assign', $update);
783 return $returnid;
787 * Delete all grades from the gradebook for this assignment.
789 * @return bool
791 protected function delete_grades() {
792 global $CFG;
794 $result = grade_update('mod/assign',
795 $this->get_course()->id,
796 'mod',
797 'assign',
798 $this->get_instance()->id,
800 null,
801 array('deleted'=>1));
802 return $result == GRADE_UPDATE_OK;
806 * Delete this instance from the database.
808 * @return bool false if an error occurs
810 public function delete_instance() {
811 global $DB;
812 $result = true;
814 foreach ($this->submissionplugins as $plugin) {
815 if (!$plugin->delete_instance()) {
816 print_error($plugin->get_error());
817 $result = false;
820 foreach ($this->feedbackplugins as $plugin) {
821 if (!$plugin->delete_instance()) {
822 print_error($plugin->get_error());
823 $result = false;
827 // Delete files associated with this assignment.
828 $fs = get_file_storage();
829 if (! $fs->delete_area_files($this->context->id) ) {
830 $result = false;
833 $this->delete_all_overrides();
835 // Delete_records will throw an exception if it fails - so no need for error checking here.
836 $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id));
837 $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id));
838 $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id));
839 $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id));
840 $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id));
842 // Delete items from the gradebook.
843 if (! $this->delete_grades()) {
844 $result = false;
847 // Delete the instance.
848 // We must delete the module record after we delete the grade item.
849 $DB->delete_records('assign', array('id'=>$this->get_instance()->id));
851 return $result;
855 * Deletes a assign override from the database and clears any corresponding calendar events
857 * @param int $overrideid The id of the override being deleted
858 * @return bool true on success
860 public function delete_override($overrideid) {
861 global $CFG, $DB;
863 require_once($CFG->dirroot . '/calendar/lib.php');
865 $cm = $this->get_course_module();
866 if (empty($cm)) {
867 $instance = $this->get_instance();
868 $cm = get_coursemodule_from_instance('assign', $instance->id, $instance->course);
871 $override = $DB->get_record('assign_overrides', array('id' => $overrideid), '*', MUST_EXIST);
873 // Delete the events.
874 $conds = array('modulename' => 'assign', 'instance' => $this->get_instance()->id);
875 if (isset($override->userid)) {
876 $conds['userid'] = $override->userid;
877 $cachekey = "{$cm->instance}_u_{$override->userid}";
878 } else {
879 $conds['groupid'] = $override->groupid;
880 $cachekey = "{$cm->instance}_g_{$override->groupid}";
882 $events = $DB->get_records('event', $conds);
883 foreach ($events as $event) {
884 $eventold = calendar_event::load($event);
885 $eventold->delete();
888 $DB->delete_records('assign_overrides', array('id' => $overrideid));
889 cache::make('mod_assign', 'overrides')->delete($cachekey);
891 // Set the common parameters for one of the events we will be triggering.
892 $params = array(
893 'objectid' => $override->id,
894 'context' => context_module::instance($cm->id),
895 'other' => array(
896 'assignid' => $override->assignid
899 // Determine which override deleted event to fire.
900 if (!empty($override->userid)) {
901 $params['relateduserid'] = $override->userid;
902 $event = \mod_assign\event\user_override_deleted::create($params);
903 } else {
904 $params['other']['groupid'] = $override->groupid;
905 $event = \mod_assign\event\group_override_deleted::create($params);
908 // Trigger the override deleted event.
909 $event->add_record_snapshot('assign_overrides', $override);
910 $event->trigger();
912 return true;
916 * Deletes all assign overrides from the database and clears any corresponding calendar events
918 public function delete_all_overrides() {
919 global $DB;
921 $overrides = $DB->get_records('assign_overrides', array('assignid' => $this->get_instance()->id), 'id');
922 foreach ($overrides as $override) {
923 $this->delete_override($override->id);
928 * Updates the assign properties with override information for a user.
930 * Algorithm: For each assign setting, if there is a matching user-specific override,
931 * then use that otherwise, if there are group-specific overrides, return the most
932 * lenient combination of them. If neither applies, leave the assign setting unchanged.
934 * @param int $userid The userid.
936 public function update_effective_access($userid) {
938 $override = $this->override_exists($userid);
940 // Merge with assign defaults.
941 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
942 foreach ($keys as $key) {
943 if (isset($override->{$key})) {
944 $this->get_instance($userid)->{$key} = $override->{$key};
951 * Returns whether an assign has any overrides.
953 * @return true if any, false if not
955 public function has_overrides() {
956 global $DB;
958 $override = $DB->record_exists('assign_overrides', array('assignid' => $this->get_instance()->id));
960 if ($override) {
961 return true;
964 return false;
968 * Returns user override
970 * Algorithm: For each assign setting, if there is a matching user-specific override,
971 * then use that otherwise, if there are group-specific overrides, use the one with the
972 * lowest sort order. If neither applies, leave the assign setting unchanged.
974 * @param int $userid The userid.
975 * @return stdClass The override
977 public function override_exists($userid) {
978 global $DB;
980 // Gets an assoc array containing the keys for defined user overrides only.
981 $getuseroverride = function($userid) use ($DB) {
982 $useroverride = $DB->get_record('assign_overrides', ['assignid' => $this->get_instance()->id, 'userid' => $userid]);
983 return $useroverride ? get_object_vars($useroverride) : [];
986 // Gets an assoc array containing the keys for defined group overrides only.
987 $getgroupoverride = function($userid) use ($DB) {
988 $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
990 if (empty($groupings[0])) {
991 return [];
994 // Select all overrides that apply to the User's groups.
995 list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
996 $sql = "SELECT * FROM {assign_overrides}
997 WHERE groupid $extra AND assignid = ? ORDER BY sortorder ASC";
998 $params[] = $this->get_instance()->id;
999 $groupoverride = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
1001 return $groupoverride ? get_object_vars($groupoverride) : [];
1004 // Later arguments clobber earlier ones with array_merge. The two helper functions
1005 // return arrays containing keys for only the defined overrides. So we get the
1006 // desired behaviour as per the algorithm.
1007 return (object)array_merge(
1008 ['duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null],
1009 $getgroupoverride($userid),
1010 $getuseroverride($userid)
1015 * Check if the given calendar_event is either a user or group override
1016 * event.
1018 * @return bool
1020 public function is_override_calendar_event(\calendar_event $event) {
1021 global $DB;
1023 if (!isset($event->modulename)) {
1024 return false;
1027 if ($event->modulename != 'assign') {
1028 return false;
1031 if (!isset($event->instance)) {
1032 return false;
1035 if (!isset($event->userid) && !isset($event->groupid)) {
1036 return false;
1039 $overrideparams = [
1040 'assignid' => $event->instance
1043 if (isset($event->groupid)) {
1044 $overrideparams['groupid'] = $event->groupid;
1045 } else if (isset($event->userid)) {
1046 $overrideparams['userid'] = $event->userid;
1049 if ($DB->get_record('assign_overrides', $overrideparams)) {
1050 return true;
1051 } else {
1052 return false;
1057 * This function calculates the minimum and maximum cutoff values for the timestart of
1058 * the given event.
1060 * It will return an array with two values, the first being the minimum cutoff value and
1061 * the second being the maximum cutoff value. Either or both values can be null, which
1062 * indicates there is no minimum or maximum, respectively.
1064 * If a cutoff is required then the function must return an array containing the cutoff
1065 * timestamp and error string to display to the user if the cutoff value is violated.
1067 * A minimum and maximum cutoff return value will look like:
1069 * [1505704373, 'The due date must be after the sbumission start date'],
1070 * [1506741172, 'The due date must be before the cutoff date']
1073 * If the event does not have a valid timestart range then [false, false] will
1074 * be returned.
1076 * @param calendar_event $event The calendar event to get the time range for
1077 * @return array
1079 function get_valid_calendar_event_timestart_range(\calendar_event $event) {
1080 $instance = $this->get_instance();
1081 $submissionsfromdate = $instance->allowsubmissionsfromdate;
1082 $cutoffdate = $instance->cutoffdate;
1083 $duedate = $instance->duedate;
1084 $gradingduedate = $instance->gradingduedate;
1085 $mindate = null;
1086 $maxdate = null;
1088 if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
1089 // This check is in here because due date events are currently
1090 // the only events that can be overridden, so we can save a DB
1091 // query if we don't bother checking other events.
1092 if ($this->is_override_calendar_event($event)) {
1093 // This is an override event so there is no valid timestart
1094 // range to set it to.
1095 return [false, false];
1098 if ($submissionsfromdate) {
1099 $mindate = [
1100 $submissionsfromdate,
1101 get_string('duedatevalidation', 'assign'),
1105 if ($cutoffdate) {
1106 $maxdate = [
1107 $cutoffdate,
1108 get_string('cutoffdatevalidation', 'assign'),
1112 if ($gradingduedate) {
1113 // If we don't have a cutoff date or we've got a grading due date
1114 // that is earlier than the cutoff then we should use that as the
1115 // upper limit for the due date.
1116 if (!$cutoffdate || $gradingduedate < $cutoffdate) {
1117 $maxdate = [
1118 $gradingduedate,
1119 get_string('gradingdueduedatevalidation', 'assign'),
1123 } else if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) {
1124 if ($duedate) {
1125 $mindate = [
1126 $duedate,
1127 get_string('gradingdueduedatevalidation', 'assign'),
1129 } else if ($submissionsfromdate) {
1130 $mindate = [
1131 $submissionsfromdate,
1132 get_string('gradingduefromdatevalidation', 'assign'),
1137 return [$mindate, $maxdate];
1141 * Actual implementation of the reset course functionality, delete all the
1142 * assignment submissions for course $data->courseid.
1144 * @param stdClass $data the data submitted from the reset course.
1145 * @return array status array
1147 public function reset_userdata($data) {
1148 global $CFG, $DB;
1150 $componentstr = get_string('modulenameplural', 'assign');
1151 $status = array();
1153 $fs = get_file_storage();
1154 if (!empty($data->reset_assign_submissions)) {
1155 // Delete files associated with this assignment.
1156 foreach ($this->submissionplugins as $plugin) {
1157 $fileareas = array();
1158 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1159 $fileareas = $plugin->get_file_areas();
1160 foreach ($fileareas as $filearea => $notused) {
1161 $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1164 if (!$plugin->delete_instance()) {
1165 $status[] = array('component'=>$componentstr,
1166 'item'=>get_string('deleteallsubmissions', 'assign'),
1167 'error'=>$plugin->get_error());
1171 foreach ($this->feedbackplugins as $plugin) {
1172 $fileareas = array();
1173 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1174 $fileareas = $plugin->get_file_areas();
1175 foreach ($fileareas as $filearea => $notused) {
1176 $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1179 if (!$plugin->delete_instance()) {
1180 $status[] = array('component'=>$componentstr,
1181 'item'=>get_string('deleteallsubmissions', 'assign'),
1182 'error'=>$plugin->get_error());
1186 $assignids = $DB->get_records('assign', array('course' => $data->courseid), '', 'id');
1187 list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids));
1189 $DB->delete_records_select('assign_submission', "assignment $sql", $params);
1190 $DB->delete_records_select('assign_user_flags', "assignment $sql", $params);
1192 $status[] = array('component'=>$componentstr,
1193 'item'=>get_string('deleteallsubmissions', 'assign'),
1194 'error'=>false);
1196 if (!empty($data->reset_gradebook_grades)) {
1197 $DB->delete_records_select('assign_grades', "assignment $sql", $params);
1198 // Remove all grades from gradebook.
1199 require_once($CFG->dirroot.'/mod/assign/lib.php');
1200 assign_reset_gradebook($data->courseid);
1203 // Reset revealidentities for assign if blindmarking is enabled.
1204 if ($this->get_instance()->blindmarking) {
1205 $DB->set_field('assign', 'revealidentities', 0, array('id' => $this->get_instance()->id));
1209 $purgeoverrides = false;
1211 // Remove user overrides.
1212 if (!empty($data->reset_assign_user_overrides)) {
1213 $DB->delete_records_select('assign_overrides',
1214 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid));
1215 $status[] = array(
1216 'component' => $componentstr,
1217 'item' => get_string('useroverridesdeleted', 'assign'),
1218 'error' => false);
1219 $purgeoverrides = true;
1221 // Remove group overrides.
1222 if (!empty($data->reset_assign_group_overrides)) {
1223 $DB->delete_records_select('assign_overrides',
1224 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid));
1225 $status[] = array(
1226 'component' => $componentstr,
1227 'item' => get_string('groupoverridesdeleted', 'assign'),
1228 'error' => false);
1229 $purgeoverrides = true;
1232 // Updating dates - shift may be negative too.
1233 if ($data->timeshift) {
1234 $DB->execute("UPDATE {assign_overrides}
1235 SET allowsubmissionsfromdate = allowsubmissionsfromdate + ?
1236 WHERE assignid = ? AND allowsubmissionsfromdate <> 0",
1237 array($data->timeshift, $this->get_instance()->id));
1238 $DB->execute("UPDATE {assign_overrides}
1239 SET duedate = duedate + ?
1240 WHERE assignid = ? AND duedate <> 0",
1241 array($data->timeshift, $this->get_instance()->id));
1242 $DB->execute("UPDATE {assign_overrides}
1243 SET cutoffdate = cutoffdate + ?
1244 WHERE assignid =? AND cutoffdate <> 0",
1245 array($data->timeshift, $this->get_instance()->id));
1247 $purgeoverrides = true;
1249 // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
1250 // See MDL-9367.
1251 shift_course_mod_dates('assign',
1252 array('duedate', 'allowsubmissionsfromdate', 'cutoffdate'),
1253 $data->timeshift,
1254 $data->courseid, $this->get_instance()->id);
1255 $status[] = array('component'=>$componentstr,
1256 'item'=>get_string('datechanged'),
1257 'error'=>false);
1260 if ($purgeoverrides) {
1261 cache::make('mod_assign', 'overrides')->purge();
1264 return $status;
1268 * Update the settings for a single plugin.
1270 * @param assign_plugin $plugin The plugin to update
1271 * @param stdClass $formdata The form data
1272 * @return bool false if an error occurs
1274 protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) {
1275 if ($plugin->is_visible()) {
1276 $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1277 if (!empty($formdata->$enabledname)) {
1278 $plugin->enable();
1279 if (!$plugin->save_settings($formdata)) {
1280 print_error($plugin->get_error());
1281 return false;
1283 } else {
1284 $plugin->disable();
1287 return true;
1291 * Update the gradebook information for this assignment.
1293 * @param bool $reset If true, will reset all grades in the gradbook for this assignment
1294 * @param int $coursemoduleid This is required because it might not exist in the database yet
1295 * @return bool
1297 public function update_gradebook($reset, $coursemoduleid) {
1298 global $CFG;
1300 require_once($CFG->dirroot.'/mod/assign/lib.php');
1301 $assign = clone $this->get_instance();
1302 $assign->cmidnumber = $coursemoduleid;
1304 // Set assign gradebook feedback plugin status (enabled and visible).
1305 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
1307 $param = null;
1308 if ($reset) {
1309 $param = 'reset';
1312 return assign_grade_item_update($assign, $param);
1316 * Get the marking table page size
1318 * @return integer
1320 public function get_assign_perpage() {
1321 $perpage = (int) get_user_preferences('assign_perpage', 10);
1322 $adminconfig = $this->get_admin_config();
1323 $maxperpage = -1;
1324 if (isset($adminconfig->maxperpage)) {
1325 $maxperpage = $adminconfig->maxperpage;
1327 if (isset($maxperpage) &&
1328 $maxperpage != -1 &&
1329 ($perpage == -1 || $perpage > $maxperpage)) {
1330 $perpage = $maxperpage;
1332 return $perpage;
1336 * Load and cache the admin config for this module.
1338 * @return stdClass the plugin config
1340 public function get_admin_config() {
1341 if ($this->adminconfig) {
1342 return $this->adminconfig;
1344 $this->adminconfig = get_config('assign');
1345 return $this->adminconfig;
1349 * Update the calendar entries for this assignment.
1351 * @param int $coursemoduleid - Required to pass this in because it might
1352 * not exist in the database yet.
1353 * @return bool
1355 public function update_calendar($coursemoduleid) {
1356 global $DB, $CFG;
1357 require_once($CFG->dirroot.'/calendar/lib.php');
1359 // Special case for add_instance as the coursemodule has not been set yet.
1360 $instance = $this->get_instance();
1362 // Start with creating the event.
1363 $event = new stdClass();
1364 $event->modulename = 'assign';
1365 $event->courseid = $instance->course;
1366 $event->groupid = 0;
1367 $event->userid = 0;
1368 $event->instance = $instance->id;
1369 $event->type = CALENDAR_EVENT_TYPE_ACTION;
1371 // Convert the links to pluginfile. It is a bit hacky but at this stage the files
1372 // might not have been saved in the module area yet.
1373 $intro = $instance->intro;
1374 if ($draftid = file_get_submitted_draft_itemid('introeditor')) {
1375 $intro = file_rewrite_urls_to_pluginfile($intro, $draftid);
1378 // We need to remove the links to files as the calendar is not ready
1379 // to support module events with file areas.
1380 $intro = strip_pluginfile_content($intro);
1381 if ($this->show_intro()) {
1382 $event->description = array(
1383 'text' => $intro,
1384 'format' => $instance->introformat
1386 } else {
1387 $event->description = array(
1388 'text' => '',
1389 'format' => $instance->introformat
1393 $eventtype = ASSIGN_EVENT_TYPE_DUE;
1394 if ($instance->duedate) {
1395 $event->name = get_string('calendardue', 'assign', $instance->name);
1396 $event->eventtype = $eventtype;
1397 $event->timestart = $instance->duedate;
1398 $event->timesort = $instance->duedate;
1399 $select = "modulename = :modulename
1400 AND instance = :instance
1401 AND eventtype = :eventtype
1402 AND groupid = 0
1403 AND courseid <> 0";
1404 $params = array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype);
1405 $event->id = $DB->get_field_select('event', 'id', $select, $params);
1407 // Now process the event.
1408 if ($event->id) {
1409 $calendarevent = calendar_event::load($event->id);
1410 $calendarevent->update($event, false);
1411 } else {
1412 calendar_event::create($event, false);
1414 } else {
1415 $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1416 'eventtype' => $eventtype));
1419 $eventtype = ASSIGN_EVENT_TYPE_GRADINGDUE;
1420 if ($instance->gradingduedate) {
1421 $event->name = get_string('calendargradingdue', 'assign', $instance->name);
1422 $event->eventtype = $eventtype;
1423 $event->timestart = $instance->gradingduedate;
1424 $event->timesort = $instance->gradingduedate;
1425 $event->id = $DB->get_field('event', 'id', array('modulename' => 'assign',
1426 'instance' => $instance->id, 'eventtype' => $event->eventtype));
1428 // Now process the event.
1429 if ($event->id) {
1430 $calendarevent = calendar_event::load($event->id);
1431 $calendarevent->update($event, false);
1432 } else {
1433 calendar_event::create($event, false);
1435 } else {
1436 $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1437 'eventtype' => $eventtype));
1440 return true;
1444 * Update this instance in the database.
1446 * @param stdClass $formdata - the data submitted from the form
1447 * @return bool false if an error occurs
1449 public function update_instance($formdata) {
1450 global $DB;
1451 $adminconfig = $this->get_admin_config();
1453 $update = new stdClass();
1454 $update->id = $formdata->instance;
1455 $update->name = $formdata->name;
1456 $update->timemodified = time();
1457 $update->course = $formdata->course;
1458 $update->intro = $formdata->intro;
1459 $update->introformat = $formdata->introformat;
1460 $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
1461 $update->submissiondrafts = $formdata->submissiondrafts;
1462 $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
1463 $update->sendnotifications = $formdata->sendnotifications;
1464 $update->sendlatenotifications = $formdata->sendlatenotifications;
1465 $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
1466 if (isset($formdata->sendstudentnotifications)) {
1467 $update->sendstudentnotifications = $formdata->sendstudentnotifications;
1469 $update->duedate = $formdata->duedate;
1470 $update->cutoffdate = $formdata->cutoffdate;
1471 $update->gradingduedate = $formdata->gradingduedate;
1472 $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
1473 $update->grade = $formdata->grade;
1474 if (!empty($formdata->completionunlocked)) {
1475 $update->completionsubmit = !empty($formdata->completionsubmit);
1477 $update->teamsubmission = $formdata->teamsubmission;
1478 $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
1479 if (isset($formdata->teamsubmissiongroupingid)) {
1480 $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
1482 if (isset($formdata->hidegrader)) {
1483 $update->hidegrader = $formdata->hidegrader;
1485 $update->blindmarking = $formdata->blindmarking;
1486 $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
1487 if (!empty($formdata->attemptreopenmethod)) {
1488 $update->attemptreopenmethod = $formdata->attemptreopenmethod;
1490 if (!empty($formdata->maxattempts)) {
1491 $update->maxattempts = $formdata->maxattempts;
1493 if (isset($formdata->preventsubmissionnotingroup)) {
1494 $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
1496 $update->markingworkflow = $formdata->markingworkflow;
1497 $update->markingallocation = $formdata->markingallocation;
1498 if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
1499 $update->markingallocation = 0;
1502 $result = $DB->update_record('assign', $update);
1503 $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
1505 $this->save_intro_draft_files($formdata);
1507 // Load the assignment so the plugins have access to it.
1509 // Call save_settings hook for submission plugins.
1510 foreach ($this->submissionplugins as $plugin) {
1511 if (!$this->update_plugin_instance($plugin, $formdata)) {
1512 print_error($plugin->get_error());
1513 return false;
1516 foreach ($this->feedbackplugins as $plugin) {
1517 if (!$this->update_plugin_instance($plugin, $formdata)) {
1518 print_error($plugin->get_error());
1519 return false;
1523 $this->update_calendar($this->get_course_module()->id);
1524 $completionexpected = (!empty($formdata->completionexpected)) ? $formdata->completionexpected : null;
1525 \core_completion\api::update_completion_date_event($this->get_course_module()->id, 'assign', $this->instance,
1526 $completionexpected);
1527 $this->update_gradebook(false, $this->get_course_module()->id);
1529 $update = new stdClass();
1530 $update->id = $this->get_instance()->id;
1531 $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
1532 $DB->update_record('assign', $update);
1534 return $result;
1538 * Save the attachments in the draft areas.
1540 * @param stdClass $formdata
1542 protected function save_intro_draft_files($formdata) {
1543 if (isset($formdata->introattachments)) {
1544 file_save_draft_area_files($formdata->introattachments, $this->get_context()->id,
1545 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
1550 * Add elements in grading plugin form.
1552 * @param mixed $grade stdClass|null
1553 * @param MoodleQuickForm $mform
1554 * @param stdClass $data
1555 * @param int $userid - The userid we are grading
1556 * @return void
1558 protected function add_plugin_grade_elements($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
1559 foreach ($this->feedbackplugins as $plugin) {
1560 if ($plugin->is_enabled() && $plugin->is_visible()) {
1561 $plugin->get_form_elements_for_user($grade, $mform, $data, $userid);
1569 * Add one plugins settings to edit plugin form.
1571 * @param assign_plugin $plugin The plugin to add the settings from
1572 * @param MoodleQuickForm $mform The form to add the configuration settings to.
1573 * This form is modified directly (not returned).
1574 * @param array $pluginsenabled A list of form elements to be added to a group.
1575 * The new element is added to this array by this function.
1576 * @return void
1578 protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) {
1579 global $CFG;
1580 if ($plugin->is_visible() && !$plugin->is_configurable() && $plugin->is_enabled()) {
1581 $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1582 $pluginsenabled[] = $mform->createElement('hidden', $name, 1);
1583 $mform->setType($name, PARAM_BOOL);
1584 $plugin->get_settings($mform);
1585 } else if ($plugin->is_visible() && $plugin->is_configurable()) {
1586 $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1587 $label = $plugin->get_name();
1588 $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label);
1589 $helpicon = $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type());
1590 $pluginsenabled[] = $mform->createElement('static', '', '', $helpicon);
1592 $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default');
1593 if ($plugin->get_config('enabled') !== false) {
1594 $default = $plugin->is_enabled();
1596 $mform->setDefault($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled', $default);
1598 $plugin->get_settings($mform);
1604 * Add settings to edit plugin form.
1606 * @param MoodleQuickForm $mform The form to add the configuration settings to.
1607 * This form is modified directly (not returned).
1608 * @return void
1610 public function add_all_plugin_settings(MoodleQuickForm $mform) {
1611 $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign'));
1613 $submissionpluginsenabled = array();
1614 $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false);
1615 foreach ($this->submissionplugins as $plugin) {
1616 $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled);
1618 $group->setElements($submissionpluginsenabled);
1620 $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign'));
1621 $feedbackpluginsenabled = array();
1622 $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false);
1623 foreach ($this->feedbackplugins as $plugin) {
1624 $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled);
1626 $group->setElements($feedbackpluginsenabled);
1627 $mform->setExpanded('submissiontypes');
1631 * Allow each plugin an opportunity to update the defaultvalues
1632 * passed in to the settings form (needed to set up draft areas for
1633 * editor and filemanager elements)
1635 * @param array $defaultvalues
1637 public function plugin_data_preprocessing(&$defaultvalues) {
1638 foreach ($this->submissionplugins as $plugin) {
1639 if ($plugin->is_visible()) {
1640 $plugin->data_preprocessing($defaultvalues);
1643 foreach ($this->feedbackplugins as $plugin) {
1644 if ($plugin->is_visible()) {
1645 $plugin->data_preprocessing($defaultvalues);
1651 * Get the name of the current module.
1653 * @return string the module name (Assignment)
1655 protected function get_module_name() {
1656 if (isset(self::$modulename)) {
1657 return self::$modulename;
1659 self::$modulename = get_string('modulename', 'assign');
1660 return self::$modulename;
1664 * Get the plural name of the current module.
1666 * @return string the module name plural (Assignments)
1668 protected function get_module_name_plural() {
1669 if (isset(self::$modulenameplural)) {
1670 return self::$modulenameplural;
1672 self::$modulenameplural = get_string('modulenameplural', 'assign');
1673 return self::$modulenameplural;
1677 * Has this assignment been constructed from an instance?
1679 * @return bool
1681 public function has_instance() {
1682 return $this->instance || $this->get_course_module();
1686 * Get the settings for the current instance of this assignment.
1688 * @return stdClass The settings
1689 * @throws dml_exception
1691 public function get_default_instance() {
1692 global $DB;
1693 if (!$this->instance && $this->get_course_module()) {
1694 $params = array('id' => $this->get_course_module()->instance);
1695 $this->instance = $DB->get_record('assign', $params, '*', MUST_EXIST);
1697 $this->userinstances = [];
1699 return $this->instance;
1703 * Get the settings for the current instance of this assignment
1704 * @param int|null $userid the id of the user to load the assign instance for.
1705 * @return stdClass The settings
1707 public function get_instance(int $userid = null) : stdClass {
1708 global $USER;
1709 $userid = $userid ?? $USER->id;
1711 $this->instance = $this->get_default_instance();
1713 // If we have the user instance already, just return it.
1714 if (isset($this->userinstances[$userid])) {
1715 return $this->userinstances[$userid];
1718 // Calculate properties which vary per user.
1719 $this->userinstances[$userid] = $this->calculate_properties($this->instance, $userid);
1720 return $this->userinstances[$userid];
1724 * Calculates and updates various properties based on the specified user.
1726 * @param stdClass $record the raw assign record.
1727 * @param int $userid the id of the user to calculate the properties for.
1728 * @return stdClass a new record having calculated properties.
1730 private function calculate_properties(\stdClass $record, int $userid) : \stdClass {
1731 $record = clone ($record);
1733 // Relative dates.
1734 if (!empty($record->duedate)) {
1735 $course = $this->get_course();
1736 $usercoursedates = course_get_course_dates_for_user_id($course, $userid);
1737 if ($usercoursedates['start']) {
1738 $userprops = ['duedate' => $record->duedate + $usercoursedates['startoffset']];
1739 $record = (object) array_merge((array) $record, (array) $userprops);
1742 return $record;
1746 * Get the primary grade item for this assign instance.
1748 * @return grade_item The grade_item record
1750 public function get_grade_item() {
1751 if ($this->gradeitem) {
1752 return $this->gradeitem;
1754 $instance = $this->get_instance();
1755 $params = array('itemtype' => 'mod',
1756 'itemmodule' => 'assign',
1757 'iteminstance' => $instance->id,
1758 'courseid' => $instance->course,
1759 'itemnumber' => 0);
1760 $this->gradeitem = grade_item::fetch($params);
1761 if (!$this->gradeitem) {
1762 throw new coding_exception('Improper use of the assignment class. ' .
1763 'Cannot load the grade item.');
1765 return $this->gradeitem;
1769 * Get the context of the current course.
1771 * @return mixed context|null The course context
1773 public function get_course_context() {
1774 if (!$this->context && !$this->course) {
1775 throw new coding_exception('Improper use of the assignment class. ' .
1776 'Cannot load the course context.');
1778 if ($this->context) {
1779 return $this->context->get_course_context();
1780 } else {
1781 return context_course::instance($this->course->id);
1787 * Get the current course module.
1789 * @return cm_info|null The course module or null if not known
1791 public function get_course_module() {
1792 if ($this->coursemodule) {
1793 return $this->coursemodule;
1795 if (!$this->context) {
1796 return null;
1799 if ($this->context->contextlevel == CONTEXT_MODULE) {
1800 $modinfo = get_fast_modinfo($this->get_course());
1801 $this->coursemodule = $modinfo->get_cm($this->context->instanceid);
1802 return $this->coursemodule;
1804 return null;
1808 * Get context module.
1810 * @return context
1812 public function get_context() {
1813 return $this->context;
1817 * Get the current course.
1819 * @return mixed stdClass|null The course
1821 public function get_course() {
1822 global $DB;
1824 if ($this->course && is_object($this->course)) {
1825 return $this->course;
1828 if (!$this->context) {
1829 return null;
1831 $params = array('id' => $this->get_course_context()->instanceid);
1832 $this->course = $DB->get_record('course', $params, '*', MUST_EXIST);
1834 return $this->course;
1838 * Count the number of intro attachments.
1840 * @return int
1842 protected function count_attachments() {
1844 $fs = get_file_storage();
1845 $files = $fs->get_area_files($this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA,
1846 0, 'id', false);
1848 return count($files);
1852 * Are there any intro attachments to display?
1854 * @return boolean
1856 protected function has_visible_attachments() {
1857 return ($this->count_attachments() > 0);
1861 * Return a grade in user-friendly form, whether it's a scale or not.
1863 * @param mixed $grade int|null
1864 * @param boolean $editing Are we allowing changes to this grade?
1865 * @param int $userid The user id the grade belongs to
1866 * @param int $modified Timestamp from when the grade was last modified
1867 * @return string User-friendly representation of grade
1869 public function display_grade($grade, $editing, $userid=0, $modified=0) {
1870 global $DB;
1872 static $scalegrades = array();
1874 $o = '';
1876 if ($this->get_instance()->grade >= 0) {
1877 // Normal number.
1878 if ($editing && $this->get_instance()->grade > 0) {
1879 if ($grade < 0) {
1880 $displaygrade = '';
1881 } else {
1882 $displaygrade = format_float($grade, $this->get_grade_item()->get_decimals());
1884 $o .= '<label class="accesshide" for="quickgrade_' . $userid . '">' .
1885 get_string('usergrade', 'assign') .
1886 '</label>';
1887 $o .= '<input type="text"
1888 id="quickgrade_' . $userid . '"
1889 name="quickgrade_' . $userid . '"
1890 value="' . $displaygrade . '"
1891 size="6"
1892 maxlength="10"
1893 class="quickgrade"/>';
1894 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $this->get_grade_item()->get_decimals());
1895 return $o;
1896 } else {
1897 if ($grade == -1 || $grade === null) {
1898 $o .= '-';
1899 } else {
1900 $item = $this->get_grade_item();
1901 $o .= grade_format_gradevalue($grade, $item);
1902 if ($item->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
1903 // If displaying the raw grade, also display the total value.
1904 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $item->get_decimals());
1907 return $o;
1910 } else {
1911 // Scale.
1912 if (empty($this->cache['scale'])) {
1913 if ($scale = $DB->get_record('scale', array('id'=>-($this->get_instance()->grade)))) {
1914 $this->cache['scale'] = make_menu_from_list($scale->scale);
1915 } else {
1916 $o .= '-';
1917 return $o;
1920 if ($editing) {
1921 $o .= '<label class="accesshide"
1922 for="quickgrade_' . $userid . '">' .
1923 get_string('usergrade', 'assign') .
1924 '</label>';
1925 $o .= '<select name="quickgrade_' . $userid . '" class="quickgrade">';
1926 $o .= '<option value="-1">' . get_string('nograde') . '</option>';
1927 foreach ($this->cache['scale'] as $optionid => $option) {
1928 $selected = '';
1929 if ($grade == $optionid) {
1930 $selected = 'selected="selected"';
1932 $o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>';
1934 $o .= '</select>';
1935 return $o;
1936 } else {
1937 $scaleid = (int)$grade;
1938 if (isset($this->cache['scale'][$scaleid])) {
1939 $o .= $this->cache['scale'][$scaleid];
1940 return $o;
1942 $o .= '-';
1943 return $o;
1949 * Get the submission status/grading status for all submissions in this assignment for the
1950 * given paticipants.
1952 * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
1953 * If this is a group assignment, group info is also returned.
1955 * @param array $participants an associative array where the key is the participant id and
1956 * the value is the participant record.
1957 * @return array an associative array where the key is the participant id and the value is
1958 * the participant record.
1960 private function get_submission_info_for_participants($participants) {
1961 global $DB;
1963 if (empty($participants)) {
1964 return $participants;
1967 list($insql, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
1969 $assignid = $this->get_instance()->id;
1970 $params['assignmentid1'] = $assignid;
1971 $params['assignmentid2'] = $assignid;
1972 $params['assignmentid3'] = $assignid;
1974 $fields = 'SELECT u.id, s.status, s.timemodified AS stime, g.timemodified AS gtime, g.grade, uf.extensionduedate';
1975 $from = ' FROM {user} u
1976 LEFT JOIN {assign_submission} s
1977 ON u.id = s.userid
1978 AND s.assignment = :assignmentid1
1979 AND s.latest = 1
1980 LEFT JOIN {assign_grades} g
1981 ON u.id = g.userid
1982 AND g.assignment = :assignmentid2
1983 AND g.attemptnumber = s.attemptnumber
1984 LEFT JOIN {assign_user_flags} uf
1985 ON u.id = uf.userid
1986 AND uf.assignment = :assignmentid3
1988 $where = ' WHERE u.id ' . $insql;
1990 if (!empty($this->get_instance()->blindmarking)) {
1991 $from .= 'LEFT JOIN {assign_user_mapping} um
1992 ON u.id = um.userid
1993 AND um.assignment = :assignmentid4 ';
1994 $params['assignmentid4'] = $assignid;
1995 $fields .= ', um.id as recordid ';
1998 $sql = "$fields $from $where";
2000 $records = $DB->get_records_sql($sql, $params);
2002 if ($this->get_instance()->teamsubmission) {
2003 // Get all groups.
2004 $allgroups = groups_get_all_groups($this->get_course()->id,
2005 array_keys($participants),
2006 $this->get_instance()->teamsubmissiongroupingid,
2007 'DISTINCT g.id, g.name');
2010 foreach ($participants as $userid => $participant) {
2011 $participants[$userid]->fullname = $this->fullname($participant);
2012 $participants[$userid]->submitted = false;
2013 $participants[$userid]->requiregrading = false;
2014 $participants[$userid]->grantedextension = false;
2017 foreach ($records as $userid => $submissioninfo) {
2018 // These filters are 100% the same as the ones in the grading table SQL.
2019 $submitted = false;
2020 $requiregrading = false;
2021 $grantedextension = false;
2023 if (!empty($submissioninfo->stime) && $submissioninfo->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
2024 $submitted = true;
2027 if ($submitted && ($submissioninfo->stime >= $submissioninfo->gtime ||
2028 empty($submissioninfo->gtime) ||
2029 $submissioninfo->grade === null)) {
2030 $requiregrading = true;
2033 if (!empty($submissioninfo->extensionduedate)) {
2034 $grantedextension = true;
2037 $participants[$userid]->submitted = $submitted;
2038 $participants[$userid]->requiregrading = $requiregrading;
2039 $participants[$userid]->grantedextension = $grantedextension;
2040 if ($this->get_instance()->teamsubmission) {
2041 $group = $this->get_submission_group($userid);
2042 if ($group) {
2043 $participants[$userid]->groupid = $group->id;
2044 $participants[$userid]->groupname = $group->name;
2048 return $participants;
2052 * Get the submission status/grading status for all submissions in this assignment.
2053 * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
2054 * If this is a group assignment, group info is also returned.
2056 * @param int $currentgroup
2057 * @param boolean $tablesort Apply current user table sorting preferences.
2058 * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'grantedextension',
2059 * 'groupid', 'groupname'
2061 public function list_participants_with_filter_status_and_group($currentgroup, $tablesort = false) {
2062 $participants = $this->list_participants($currentgroup, false, $tablesort);
2064 if (empty($participants)) {
2065 return $participants;
2066 } else {
2067 return $this->get_submission_info_for_participants($participants);
2072 * Return a valid order by segment for list_participants that matches
2073 * the sorting of the current grading table. Not every field is supported,
2074 * we are only concerned with a list of users so we can't search on anything
2075 * that is not part of the user information (like grading statud or last modified stuff).
2077 * @return string Order by clause for list_participants
2079 private function get_grading_sort_sql() {
2080 $usersort = flexible_table::get_sort_for_table('mod_assign_grading');
2081 // TODO Does not support custom user profile fields (MDL-70456).
2082 $userfieldsapi = \core_user\fields::for_identity($this->context, false)->with_userpic();
2083 $userfields = $userfieldsapi->get_required_fields();
2084 $orderfields = explode(',', $usersort);
2085 $validlist = [];
2087 foreach ($orderfields as $orderfield) {
2088 $orderfield = trim($orderfield);
2089 foreach ($userfields as $field) {
2090 $parts = explode(' ', $orderfield);
2091 if ($parts[0] == $field) {
2092 // Prepend the user table prefix and count this as a valid order field.
2093 array_push($validlist, 'u.' . $orderfield);
2097 // Produce a final list.
2098 $result = implode(',', $validlist);
2099 if (empty($result)) {
2100 // Fall back ordering when none has been set.
2101 $result = 'u.lastname, u.firstname, u.id';
2104 return $result;
2108 * Returns array with sql code and parameters returning all ids of users who have submitted an assignment.
2110 * @param int $group The group that the query is for.
2111 * @return array list($sql, $params)
2113 protected function get_submitted_sql($group = 0) {
2114 // We need to guarentee unique table names.
2115 static $i = 0;
2116 $i++;
2117 $prefix = 'sa' . $i . '_';
2118 $params = [
2119 "{$prefix}assignment" => (int) $this->get_instance()->id,
2120 "{$prefix}status" => ASSIGN_SUBMISSION_STATUS_NEW,
2122 $capjoin = get_enrolled_with_capabilities_join($this->context, $prefix, '', $group, $this->show_only_active_users());
2123 $params += $capjoin->params;
2124 $sql = "SELECT {$prefix}s.userid
2125 FROM {assign_submission} {$prefix}s
2126 JOIN {user} {$prefix}u ON {$prefix}u.id = {$prefix}s.userid
2127 $capjoin->joins
2128 WHERE {$prefix}s.assignment = :{$prefix}assignment
2129 AND {$prefix}s.status <> :{$prefix}status
2130 AND $capjoin->wheres";
2131 return array($sql, $params);
2135 * Load a list of users enrolled in the current course with the specified permission and group.
2136 * 0 for no group.
2137 * Apply any current sort filters from the grading table.
2139 * @param int $currentgroup
2140 * @param bool $idsonly
2141 * @param bool $tablesort
2142 * @return array List of user records
2144 public function list_participants($currentgroup, $idsonly, $tablesort = false) {
2145 global $DB, $USER;
2147 // Get the last known sort order for the grading table.
2149 if (empty($currentgroup)) {
2150 $currentgroup = 0;
2153 $key = $this->context->id . '-' . $currentgroup . '-' . $this->show_only_active_users();
2154 if (!isset($this->participants[$key])) {
2155 list($esql, $params) = get_enrolled_sql($this->context, 'mod/assign:submit', $currentgroup,
2156 $this->show_only_active_users());
2157 list($ssql, $sparams) = $this->get_submitted_sql($currentgroup);
2158 $params += $sparams;
2160 $fields = 'u.*';
2161 $orderby = 'u.lastname, u.firstname, u.id';
2163 $additionaljoins = '';
2164 $additionalfilters = '';
2165 $instance = $this->get_instance();
2166 if (!empty($instance->blindmarking)) {
2167 $additionaljoins .= " LEFT JOIN {assign_user_mapping} um
2168 ON u.id = um.userid
2169 AND um.assignment = :assignmentid1
2170 LEFT JOIN {assign_submission} s
2171 ON u.id = s.userid
2172 AND s.assignment = :assignmentid2
2173 AND s.latest = 1
2175 $params['assignmentid1'] = (int) $instance->id;
2176 $params['assignmentid2'] = (int) $instance->id;
2177 $fields .= ', um.id as recordid ';
2179 // Sort by submission time first, then by um.id to sort reliably by the blind marking id.
2180 // Note, different DBs have different ordering of NULL values.
2181 // Therefore we coalesce the current time into the timecreated field, and the max possible integer into
2182 // the ID field.
2183 if (empty($tablesort)) {
2184 $orderby = "COALESCE(s.timecreated, " . time() . ") ASC, COALESCE(s.id, " . PHP_INT_MAX . ") ASC, um.id ASC";
2188 if ($instance->markingworkflow &&
2189 $instance->markingallocation &&
2190 !has_capability('mod/assign:manageallocations', $this->get_context()) &&
2191 has_capability('mod/assign:grade', $this->get_context())) {
2193 $additionaljoins .= ' LEFT JOIN {assign_user_flags} uf
2194 ON u.id = uf.userid
2195 AND uf.assignment = :assignmentid3';
2197 $params['assignmentid3'] = (int) $instance->id;
2199 $additionalfilters .= ' AND uf.allocatedmarker = :markerid';
2200 $params['markerid'] = $USER->id;
2203 $sql = "SELECT $fields
2204 FROM {user} u
2205 JOIN ($esql UNION $ssql) je ON je.id = u.id
2206 $additionaljoins
2207 WHERE u.deleted = 0
2208 $additionalfilters
2209 ORDER BY $orderby";
2211 $users = $DB->get_records_sql($sql, $params);
2213 $cm = $this->get_course_module();
2214 $info = new \core_availability\info_module($cm);
2215 $users = $info->filter_user_list($users);
2217 $this->participants[$key] = $users;
2220 if ($tablesort) {
2221 // Resort the user list according to the grading table sort and filter settings.
2222 $sortedfiltereduserids = $this->get_grading_userid_list(true, '');
2223 $sortedfilteredusers = [];
2224 foreach ($sortedfiltereduserids as $nextid) {
2225 $nextid = intval($nextid);
2226 if (isset($this->participants[$key][$nextid])) {
2227 $sortedfilteredusers[$nextid] = $this->participants[$key][$nextid];
2230 $this->participants[$key] = $sortedfilteredusers;
2233 if ($idsonly) {
2234 $idslist = array();
2235 foreach ($this->participants[$key] as $id => $user) {
2236 $idslist[$id] = new stdClass();
2237 $idslist[$id]->id = $id;
2239 return $idslist;
2241 return $this->participants[$key];
2245 * Load a user if they are enrolled in the current course. Populated with submission
2246 * status for this assignment.
2248 * @param int $userid
2249 * @return null|stdClass user record
2251 public function get_participant($userid) {
2252 global $DB, $USER;
2254 if ($userid == $USER->id) {
2255 $participant = clone ($USER);
2256 } else {
2257 $participant = $DB->get_record('user', array('id' => $userid));
2259 if (!$participant) {
2260 return null;
2263 if (!is_enrolled($this->context, $participant, '', $this->show_only_active_users())) {
2264 return null;
2267 $result = $this->get_submission_info_for_participants(array($participant->id => $participant));
2269 $submissioninfo = $result[$participant->id];
2270 if (!$submissioninfo->submitted && !has_capability('mod/assign:submit', $this->context, $userid)) {
2271 return null;
2274 return $submissioninfo;
2278 * Load a count of valid teams for this assignment.
2280 * @param int $activitygroup Activity active group
2281 * @return int number of valid teams
2283 public function count_teams($activitygroup = 0) {
2285 $count = 0;
2287 $participants = $this->list_participants($activitygroup, true);
2289 // If a team submission grouping id is provided all good as all returned groups
2290 // are the submission teams, but if no team submission grouping was specified
2291 // $groups will contain all participants groups.
2292 if ($this->get_instance()->teamsubmissiongroupingid) {
2294 // We restrict the users to the selected group ones.
2295 $groups = groups_get_all_groups($this->get_course()->id,
2296 array_keys($participants),
2297 $this->get_instance()->teamsubmissiongroupingid,
2298 'DISTINCT g.id, g.name');
2300 $count = count($groups);
2302 // When a specific group is selected we don't count the default group users.
2303 if ($activitygroup == 0) {
2304 if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2305 // See if there are any users in the default group.
2306 $defaultusers = $this->get_submission_group_members(0, true);
2307 if (count($defaultusers) > 0) {
2308 $count += 1;
2311 } else if ($activitygroup != 0 && empty($groups)) {
2312 // Set count to 1 if $groups returns empty.
2313 // It means the group is not part of $this->get_instance()->teamsubmissiongroupingid.
2314 $count = 1;
2316 } else {
2317 // It is faster to loop around participants if no grouping was specified.
2318 $groups = array();
2319 foreach ($participants as $participant) {
2320 if ($group = $this->get_submission_group($participant->id)) {
2321 $groups[$group->id] = true;
2322 } else if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2323 $groups[0] = true;
2327 $count = count($groups);
2330 return $count;
2334 * Load a count of active users enrolled in the current course with the specified permission and group.
2335 * 0 for no group.
2337 * @param int $currentgroup
2338 * @return int number of matching users
2340 public function count_participants($currentgroup) {
2341 return count($this->list_participants($currentgroup, true));
2345 * Load a count of active users submissions in the current module that require grading
2346 * This means the submission modification time is more recent than the
2347 * grading modification time and the status is SUBMITTED.
2349 * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
2350 * @return int number of matching submissions
2352 public function count_submissions_need_grading($currentgroup = null) {
2353 global $DB;
2355 if ($this->get_instance()->teamsubmission) {
2356 // This does not make sense for group assignment because the submission is shared.
2357 return 0;
2360 if ($currentgroup === null) {
2361 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2363 list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true);
2365 $params['assignid'] = $this->get_instance()->id;
2366 $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2367 $sqlscalegrade = $this->get_instance()->grade < 0 ? ' OR g.grade = -1' : '';
2369 $sql = 'SELECT COUNT(s.userid)
2370 FROM {assign_submission} s
2371 LEFT JOIN {assign_grades} g ON
2372 s.assignment = g.assignment AND
2373 s.userid = g.userid AND
2374 g.attemptnumber = s.attemptnumber
2375 JOIN(' . $esql . ') e ON e.id = s.userid
2376 WHERE
2377 s.latest = 1 AND
2378 s.assignment = :assignid AND
2379 s.timemodified IS NOT NULL AND
2380 s.status = :submitted AND
2381 (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL '
2382 . $sqlscalegrade . ')';
2384 return $DB->count_records_sql($sql, $params);
2388 * Load a count of grades.
2390 * @return int number of grades
2392 public function count_grades() {
2393 global $DB;
2395 if (!$this->has_instance()) {
2396 return 0;
2399 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2400 list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2402 $params['assignid'] = $this->get_instance()->id;
2404 $sql = 'SELECT COUNT(g.userid)
2405 FROM {assign_grades} g
2406 JOIN(' . $esql . ') e ON e.id = g.userid
2407 WHERE g.assignment = :assignid';
2409 return $DB->count_records_sql($sql, $params);
2413 * Load a count of submissions.
2415 * @param bool $includenew When true, also counts the submissions with status 'new'.
2416 * @return int number of submissions
2418 public function count_submissions($includenew = false) {
2419 global $DB;
2421 if (!$this->has_instance()) {
2422 return 0;
2425 $params = array();
2426 $sqlnew = '';
2428 if (!$includenew) {
2429 $sqlnew = ' AND s.status <> :status ';
2430 $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW;
2433 if ($this->get_instance()->teamsubmission) {
2434 // We cannot join on the enrolment tables for group submissions (no userid).
2435 $sql = 'SELECT COUNT(DISTINCT s.groupid)
2436 FROM {assign_submission} s
2437 WHERE
2438 s.assignment = :assignid AND
2439 s.timemodified IS NOT NULL AND
2440 s.userid = :groupuserid' .
2441 $sqlnew;
2443 $params['assignid'] = $this->get_instance()->id;
2444 $params['groupuserid'] = 0;
2445 } else {
2446 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2447 list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2449 $params = array_merge($params, $enrolparams);
2450 $params['assignid'] = $this->get_instance()->id;
2452 $sql = 'SELECT COUNT(DISTINCT s.userid)
2453 FROM {assign_submission} s
2454 JOIN(' . $esql . ') e ON e.id = s.userid
2455 WHERE
2456 s.assignment = :assignid AND
2457 s.timemodified IS NOT NULL ' .
2458 $sqlnew;
2462 return $DB->count_records_sql($sql, $params);
2466 * Load a count of submissions with a specified status.
2468 * @param string $status The submission status - should match one of the constants
2469 * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
2470 * @return int number of matching submissions
2472 public function count_submissions_with_status($status, $currentgroup = null) {
2473 global $DB;
2475 if ($currentgroup === null) {
2476 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2478 list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true);
2480 $params['assignid'] = $this->get_instance()->id;
2481 $params['assignid2'] = $this->get_instance()->id;
2482 $params['submissionstatus'] = $status;
2484 if ($this->get_instance()->teamsubmission) {
2486 $groupsstr = '';
2487 if ($currentgroup != 0) {
2488 // If there is an active group we should only display the current group users groups.
2489 $participants = $this->list_participants($currentgroup, true);
2490 $groups = groups_get_all_groups($this->get_course()->id,
2491 array_keys($participants),
2492 $this->get_instance()->teamsubmissiongroupingid,
2493 'DISTINCT g.id, g.name');
2494 if (empty($groups)) {
2495 // If $groups is empty it means it is not part of $this->get_instance()->teamsubmissiongroupingid.
2496 // All submissions from students that do not belong to any of teamsubmissiongroupingid groups
2497 // count towards groupid = 0. Setting to true as only '0' key matters.
2498 $groups = [true];
2500 list($groupssql, $groupsparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED);
2501 $groupsstr = 's.groupid ' . $groupssql . ' AND';
2502 $params = $params + $groupsparams;
2504 $sql = 'SELECT COUNT(s.groupid)
2505 FROM {assign_submission} s
2506 WHERE
2507 s.latest = 1 AND
2508 s.assignment = :assignid AND
2509 s.timemodified IS NOT NULL AND
2510 s.userid = :groupuserid AND '
2511 . $groupsstr . '
2512 s.status = :submissionstatus';
2513 $params['groupuserid'] = 0;
2514 } else {
2515 $sql = 'SELECT COUNT(s.userid)
2516 FROM {assign_submission} s
2517 JOIN(' . $esql . ') e ON e.id = s.userid
2518 WHERE
2519 s.latest = 1 AND
2520 s.assignment = :assignid AND
2521 s.timemodified IS NOT NULL AND
2522 s.status = :submissionstatus';
2526 return $DB->count_records_sql($sql, $params);
2530 * Utility function to get the userid for every row in the grading table
2531 * so the order can be frozen while we iterate it.
2533 * @param boolean $cached If true, the cached list from the session could be returned.
2534 * @param string $useridlistid String value used for caching the participant list.
2535 * @return array An array of userids
2537 protected function get_grading_userid_list($cached = false, $useridlistid = '') {
2538 global $SESSION;
2540 if ($cached) {
2541 if (empty($useridlistid)) {
2542 $useridlistid = $this->get_useridlist_key_id();
2544 $useridlistkey = $this->get_useridlist_key($useridlistid);
2545 if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) {
2546 $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list(false, '');
2548 return $SESSION->mod_assign_useridlist[$useridlistkey];
2550 $filter = get_user_preferences('assign_filter', '');
2551 $table = new assign_grading_table($this, 0, $filter, 0, false);
2553 $useridlist = $table->get_column_data('userid');
2555 return $useridlist;
2559 * Finds all assignment notifications that have yet to be mailed out, and mails them.
2561 * Cron function to be run periodically according to the moodle cron.
2563 * @return bool
2565 public static function cron() {
2566 global $DB;
2568 // Only ever send a max of one days worth of updates.
2569 $yesterday = time() - (24 * 3600);
2570 $timenow = time();
2571 $task = \core\task\manager::get_scheduled_task(mod_assign\task\cron_task::class);
2572 $lastruntime = $task->get_last_run_time();
2574 // Collect all submissions that require mailing.
2575 // Submissions are included if all are true:
2576 // - The assignment is visible in the gradebook.
2577 // - No previous notification has been sent.
2578 // - The grader was a real user, not an automated process.
2579 // - The grade was updated in the past 24 hours.
2580 // - If marking workflow is enabled, the workflow state is at 'released'.
2581 $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities, a.hidegrader,
2582 g.*, g.timemodified as lastmodified, cm.id as cmid, um.id as recordid
2583 FROM {assign} a
2584 JOIN {assign_grades} g ON g.assignment = a.id
2585 LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid
2586 JOIN {course_modules} cm ON cm.course = a.course AND cm.instance = a.id
2587 JOIN {modules} md ON md.id = cm.module AND md.name = 'assign'
2588 JOIN {grade_items} gri ON gri.iteminstance = a.id AND gri.courseid = a.course AND gri.itemmodule = md.name
2589 LEFT JOIN {assign_user_mapping} um ON g.id = um.userid AND um.assignment = a.id
2590 WHERE (a.markingworkflow = 0 OR (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND
2591 g.grader > 0 AND uf.mailed = 0 AND gri.hidden = 0 AND
2592 g.timemodified >= :yesterday AND g.timemodified <= :today
2593 ORDER BY a.course, cm.id";
2595 $params = array(
2596 'yesterday' => $yesterday,
2597 'today' => $timenow,
2598 'wfreleased' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
2600 $submissions = $DB->get_records_sql($sql, $params);
2602 if (!empty($submissions)) {
2604 mtrace('Processing ' . count($submissions) . ' assignment submissions ...');
2606 // Preload courses we are going to need those.
2607 $courseids = array();
2608 foreach ($submissions as $submission) {
2609 $courseids[] = $submission->course;
2612 // Filter out duplicates.
2613 $courseids = array_unique($courseids);
2614 $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
2615 list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
2616 $sql = 'SELECT c.*, ' . $ctxselect .
2617 ' FROM {course} c
2618 LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
2619 WHERE c.id ' . $courseidsql;
2621 $params['contextlevel'] = CONTEXT_COURSE;
2622 $courses = $DB->get_records_sql($sql, $params);
2624 // Clean up... this could go on for a while.
2625 unset($courseids);
2626 unset($ctxselect);
2627 unset($courseidsql);
2628 unset($params);
2630 // Message students about new feedback.
2631 foreach ($submissions as $submission) {
2633 mtrace("Processing assignment submission $submission->id ...");
2635 // Do not cache user lookups - could be too many.
2636 if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) {
2637 mtrace('Could not find user ' . $submission->userid);
2638 continue;
2641 // Use a cache to prevent the same DB queries happening over and over.
2642 if (!array_key_exists($submission->course, $courses)) {
2643 mtrace('Could not find course ' . $submission->course);
2644 continue;
2646 $course = $courses[$submission->course];
2647 if (isset($course->ctxid)) {
2648 // Context has not yet been preloaded. Do so now.
2649 context_helper::preload_from_record($course);
2652 // Override the language and timezone of the "current" user, so that
2653 // mail is customised for the receiver.
2654 cron_setup_user($user, $course);
2656 // Context lookups are already cached.
2657 $coursecontext = context_course::instance($course->id);
2658 if (!is_enrolled($coursecontext, $user->id)) {
2659 $courseshortname = format_string($course->shortname,
2660 true,
2661 array('context' => $coursecontext));
2662 mtrace(fullname($user) . ' not an active participant in ' . $courseshortname);
2663 continue;
2666 if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) {
2667 mtrace('Could not find grader ' . $submission->grader);
2668 continue;
2671 $modinfo = get_fast_modinfo($course, $user->id);
2672 $cm = $modinfo->get_cm($submission->cmid);
2673 // Context lookups are already cached.
2674 $contextmodule = context_module::instance($cm->id);
2676 if (!$cm->uservisible) {
2677 // Hold mail notification for assignments the user cannot access until later.
2678 continue;
2681 // Notify the student. Default to the non-anon version.
2682 $messagetype = 'feedbackavailable';
2683 // Message type needs 'anon' if "hidden grading" is enabled and the student
2684 // doesn't have permission to see the grader.
2685 if ($submission->hidegrader && !has_capability('mod/assign:showhiddengrader', $contextmodule, $user)) {
2686 $messagetype = 'feedbackavailableanon';
2687 // There's no point in having an "anonymous grader" if the notification email
2688 // comes from them. Send the email from the noreply user instead.
2689 $grader = core_user::get_noreply_user();
2692 $eventtype = 'assign_notification';
2693 $updatetime = $submission->lastmodified;
2694 $modulename = get_string('modulename', 'assign');
2696 $uniqueid = 0;
2697 if ($submission->blindmarking && !$submission->revealidentities) {
2698 if (empty($submission->recordid)) {
2699 $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $grader->id);
2700 } else {
2701 $uniqueid = $submission->recordid;
2704 $showusers = $submission->blindmarking && !$submission->revealidentities;
2705 self::send_assignment_notification($grader,
2706 $user,
2707 $messagetype,
2708 $eventtype,
2709 $updatetime,
2710 $cm,
2711 $contextmodule,
2712 $course,
2713 $modulename,
2714 $submission->name,
2715 $showusers,
2716 $uniqueid);
2718 $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
2719 if ($flags) {
2720 $flags->mailed = 1;
2721 $DB->update_record('assign_user_flags', $flags);
2722 } else {
2723 $flags = new stdClass();
2724 $flags->userid = $user->id;
2725 $flags->assignment = $submission->assignment;
2726 $flags->mailed = 1;
2727 $DB->insert_record('assign_user_flags', $flags);
2730 mtrace('Done');
2732 mtrace('Done processing ' . count($submissions) . ' assignment submissions');
2734 cron_setup_user();
2736 // Free up memory just to be sure.
2737 unset($courses);
2740 // Update calendar events to provide a description.
2741 $sql = 'SELECT id
2742 FROM {assign}
2743 WHERE
2744 allowsubmissionsfromdate >= :lastruntime AND
2745 allowsubmissionsfromdate <= :timenow AND
2746 alwaysshowdescription = 0';
2747 $params = array('lastruntime' => $lastruntime, 'timenow' => $timenow);
2748 $newlyavailable = $DB->get_records_sql($sql, $params);
2749 foreach ($newlyavailable as $record) {
2750 $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST);
2751 $context = context_module::instance($cm->id);
2753 $assignment = new assign($context, null, null);
2754 $assignment->update_calendar($cm->id);
2757 return true;
2761 * Mark in the database that this grade record should have an update notification sent by cron.
2763 * @param stdClass $grade a grade record keyed on id
2764 * @param bool $mailedoverride when true, flag notification to be sent again.
2765 * @return bool true for success
2767 public function notify_grade_modified($grade, $mailedoverride = false) {
2768 global $DB;
2770 $flags = $this->get_user_flags($grade->userid, true);
2771 if ($flags->mailed != 1 || $mailedoverride) {
2772 $flags->mailed = 0;
2775 return $this->update_user_flags($flags);
2779 * Update user flags for this user in this assignment.
2781 * @param stdClass $flags a flags record keyed on id
2782 * @return bool true for success
2784 public function update_user_flags($flags) {
2785 global $DB;
2786 if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) {
2787 return false;
2790 $result = $DB->update_record('assign_user_flags', $flags);
2791 return $result;
2795 * Update a grade in the grade table for the assignment and in the gradebook.
2797 * @param stdClass $grade a grade record keyed on id
2798 * @param bool $reopenattempt If the attempt reopen method is manual, allow another attempt at this assignment.
2799 * @return bool true for success
2801 public function update_grade($grade, $reopenattempt = false) {
2802 global $DB;
2804 $grade->timemodified = time();
2806 if (!empty($grade->workflowstate)) {
2807 $validstates = $this->get_marking_workflow_states_for_current_user();
2808 if (!array_key_exists($grade->workflowstate, $validstates)) {
2809 return false;
2813 if ($grade->grade && $grade->grade != -1) {
2814 if ($this->get_instance()->grade > 0) {
2815 if (!is_numeric($grade->grade)) {
2816 return false;
2817 } else if ($grade->grade > $this->get_instance()->grade) {
2818 return false;
2819 } else if ($grade->grade < 0) {
2820 return false;
2822 } else {
2823 // This is a scale.
2824 if ($scale = $DB->get_record('scale', array('id' => -($this->get_instance()->grade)))) {
2825 $scaleoptions = make_menu_from_list($scale->scale);
2826 if (!array_key_exists((int) $grade->grade, $scaleoptions)) {
2827 return false;
2833 if (empty($grade->attemptnumber)) {
2834 // Set it to the default.
2835 $grade->attemptnumber = 0;
2837 $DB->update_record('assign_grades', $grade);
2839 $submission = null;
2840 if ($this->get_instance()->teamsubmission) {
2841 if (isset($this->mostrecentteamsubmission)) {
2842 $submission = $this->mostrecentteamsubmission;
2843 } else {
2844 $submission = $this->get_group_submission($grade->userid, 0, false);
2846 } else {
2847 $submission = $this->get_user_submission($grade->userid, false);
2850 // Only push to gradebook if the update is for the most recent attempt.
2851 if ($submission && $submission->attemptnumber != $grade->attemptnumber) {
2852 return true;
2855 if ($this->gradebook_item_update(null, $grade)) {
2856 \mod_assign\event\submission_graded::create_from_grade($this, $grade)->trigger();
2859 // If the conditions are met, allow another attempt.
2860 if ($submission) {
2861 $this->reopen_submission_if_required($grade->userid,
2862 $submission,
2863 $reopenattempt);
2866 return true;
2870 * View the grant extension date page.
2872 * Uses url parameters 'userid'
2873 * or from parameter 'selectedusers'
2875 * @param moodleform $mform - Used for validation of the submitted data
2876 * @return string
2878 protected function view_grant_extension($mform) {
2879 global $CFG;
2880 require_once($CFG->dirroot . '/mod/assign/extensionform.php');
2882 $o = '';
2884 $data = new stdClass();
2885 $data->id = $this->get_course_module()->id;
2887 $formparams = array(
2888 'instance' => $this->get_instance(),
2889 'assign' => $this
2892 $users = optional_param('userid', 0, PARAM_INT);
2893 if (!$users) {
2894 $users = required_param('selectedusers', PARAM_SEQUENCE);
2896 $userlist = explode(',', $users);
2898 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
2899 $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
2900 foreach ($userlist as $userid) {
2901 // To validate extension date with users overrides.
2902 $override = $this->override_exists($userid);
2903 foreach ($keys as $key) {
2904 if ($override->{$key}) {
2905 if ($maxoverride[$key] < $override->{$key}) {
2906 $maxoverride[$key] = $override->{$key};
2908 } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
2909 $maxoverride[$key] = $this->get_instance()->{$key};
2913 foreach ($keys as $key) {
2914 if ($maxoverride[$key]) {
2915 $this->get_instance()->{$key} = $maxoverride[$key];
2919 $formparams['userlist'] = $userlist;
2921 $data->selectedusers = $users;
2922 $data->userid = 0;
2924 if (empty($mform)) {
2925 $mform = new mod_assign_extension_form(null, $formparams);
2927 $mform->set_data($data);
2928 $header = new assign_header($this->get_instance(),
2929 $this->get_context(),
2930 $this->show_intro(),
2931 $this->get_course_module()->id,
2932 get_string('grantextension', 'assign'));
2933 $o .= $this->get_renderer()->render($header);
2934 $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform));
2935 $o .= $this->view_footer();
2936 return $o;
2940 * Get a list of the users in the same group as this user.
2942 * @param int $groupid The id of the group whose members we want or 0 for the default group
2943 * @param bool $onlyids Whether to retrieve only the user id's
2944 * @param bool $excludesuspended Whether to exclude suspended users
2945 * @return array The users (possibly id's only)
2947 public function get_submission_group_members($groupid, $onlyids, $excludesuspended = false) {
2948 $members = array();
2949 if ($groupid != 0) {
2950 $allusers = $this->list_participants($groupid, $onlyids);
2951 foreach ($allusers as $user) {
2952 if ($this->get_submission_group($user->id)) {
2953 $members[] = $user;
2956 } else {
2957 $allusers = $this->list_participants(null, $onlyids);
2958 foreach ($allusers as $user) {
2959 if ($this->get_submission_group($user->id) == null) {
2960 $members[] = $user;
2964 // Exclude suspended users, if user can't see them.
2965 if ($excludesuspended || !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
2966 foreach ($members as $key => $member) {
2967 if (!$this->is_active_user($member->id)) {
2968 unset($members[$key]);
2973 return $members;
2977 * Get a list of the users in the same group as this user that have not submitted the assignment.
2979 * @param int $groupid The id of the group whose members we want or 0 for the default group
2980 * @param bool $onlyids Whether to retrieve only the user id's
2981 * @return array The users (possibly id's only)
2983 public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) {
2984 $instance = $this->get_instance();
2985 if (!$instance->teamsubmission || !$instance->requireallteammemberssubmit) {
2986 return array();
2988 $members = $this->get_submission_group_members($groupid, $onlyids);
2990 foreach ($members as $id => $member) {
2991 $submission = $this->get_user_submission($member->id, false);
2992 if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
2993 unset($members[$id]);
2994 } else {
2995 if ($this->is_blind_marking()) {
2996 $members[$id]->alias = get_string('hiddenuser', 'assign') .
2997 $this->get_uniqueid_for_user($id);
3001 return $members;
3005 * Load the group submission object for a particular user, optionally creating it if required.
3007 * @param int $userid The id of the user whose submission we want
3008 * @param int $groupid The id of the group for this user - may be 0 in which
3009 * case it is determined from the userid.
3010 * @param bool $create If set to true a new submission object will be created in the database
3011 * with the status set to "new".
3012 * @param int $attemptnumber - -1 means the latest attempt
3013 * @return stdClass The submission
3015 public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) {
3016 global $DB;
3018 if ($groupid == 0) {
3019 $group = $this->get_submission_group($userid);
3020 if ($group) {
3021 $groupid = $group->id;
3025 // Now get the group submission.
3026 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
3027 if ($attemptnumber >= 0) {
3028 $params['attemptnumber'] = $attemptnumber;
3031 // Only return the row with the highest attemptnumber.
3032 $submission = null;
3033 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
3034 if ($submissions) {
3035 $submission = reset($submissions);
3038 if ($submission) {
3039 return $submission;
3041 if ($create) {
3042 $submission = new stdClass();
3043 $submission->assignment = $this->get_instance()->id;
3044 $submission->userid = 0;
3045 $submission->groupid = $groupid;
3046 $submission->timecreated = time();
3047 $submission->timemodified = $submission->timecreated;
3048 if ($attemptnumber >= 0) {
3049 $submission->attemptnumber = $attemptnumber;
3050 } else {
3051 $submission->attemptnumber = 0;
3053 // Work out if this is the latest submission.
3054 $submission->latest = 0;
3055 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
3056 if ($attemptnumber == -1) {
3057 // This is a new submission so it must be the latest.
3058 $submission->latest = 1;
3059 } else {
3060 // We need to work this out.
3061 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
3062 if ($result) {
3063 $latestsubmission = reset($result);
3065 if (!$latestsubmission || ($attemptnumber == $latestsubmission->attemptnumber)) {
3066 $submission->latest = 1;
3069 if ($submission->latest) {
3070 // This is the case when we need to set latest to 0 for all the other attempts.
3071 $DB->set_field('assign_submission', 'latest', 0, $params);
3073 $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
3074 $sid = $DB->insert_record('assign_submission', $submission);
3075 return $DB->get_record('assign_submission', array('id' => $sid));
3077 return false;
3081 * View a summary listing of all assignments in the current course.
3083 * @return string
3085 private function view_course_index() {
3086 global $USER;
3088 $o = '';
3090 $course = $this->get_course();
3091 $strplural = get_string('modulenameplural', 'assign');
3093 if (!$cms = get_coursemodules_in_course('assign', $course->id, 'm.duedate')) {
3094 $o .= $this->get_renderer()->notification(get_string('thereareno', 'moodle', $strplural));
3095 $o .= $this->get_renderer()->continue_button(new moodle_url('/course/view.php', array('id' => $course->id)));
3096 return $o;
3099 $strsectionname = '';
3100 $usesections = course_format_uses_sections($course->format);
3101 $modinfo = get_fast_modinfo($course);
3103 if ($usesections) {
3104 $strsectionname = get_string('sectionname', 'format_'.$course->format);
3105 $sections = $modinfo->get_section_info_all();
3107 $courseindexsummary = new assign_course_index_summary($usesections, $strsectionname);
3109 $timenow = time();
3111 $currentsection = '';
3112 foreach ($modinfo->instances['assign'] as $cm) {
3113 if (!$cm->uservisible) {
3114 continue;
3117 $timedue = $cms[$cm->id]->duedate;
3119 $sectionname = '';
3120 if ($usesections && $cm->sectionnum) {
3121 $sectionname = get_section_name($course, $sections[$cm->sectionnum]);
3124 $submitted = '';
3125 $context = context_module::instance($cm->id);
3127 $assignment = new assign($context, $cm, $course);
3129 // Apply overrides.
3130 $assignment->update_effective_access($USER->id);
3131 $timedue = $assignment->get_instance()->duedate;
3133 if (has_capability('mod/assign:grade', $context)) {
3134 $submitted = $assignment->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED);
3136 } else if (has_capability('mod/assign:submit', $context)) {
3137 if ($assignment->get_instance()->teamsubmission) {
3138 $usersubmission = $assignment->get_group_submission($USER->id, 0, false);
3139 } else {
3140 $usersubmission = $assignment->get_user_submission($USER->id, false);
3143 if (!empty($usersubmission->status)) {
3144 $submitted = get_string('submissionstatus_' . $usersubmission->status, 'assign');
3145 } else {
3146 $submitted = get_string('submissionstatus_', 'assign');
3149 $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $cm->instance, $USER->id);
3150 if (isset($gradinginfo->items[0]->grades[$USER->id]) &&
3151 !$gradinginfo->items[0]->grades[$USER->id]->hidden ) {
3152 $grade = $gradinginfo->items[0]->grades[$USER->id]->str_grade;
3153 } else {
3154 $grade = '-';
3157 $courseindexsummary->add_assign_info($cm->id, $cm->get_formatted_name(), $sectionname, $timedue, $submitted, $grade);
3161 $o .= $this->get_renderer()->render($courseindexsummary);
3162 $o .= $this->view_footer();
3164 return $o;
3168 * View a page rendered by a plugin.
3170 * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'.
3172 * @return string
3174 protected function view_plugin_page() {
3175 global $USER;
3177 $o = '';
3179 $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
3180 $plugintype = required_param('plugin', PARAM_PLUGIN);
3181 $pluginaction = required_param('pluginaction', PARAM_ALPHA);
3183 $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
3184 if (!$plugin) {
3185 print_error('invalidformdata', '');
3186 return;
3189 $o .= $plugin->view_page($pluginaction);
3191 return $o;
3196 * This is used for team assignments to get the group for the specified user.
3197 * If the user is a member of multiple or no groups this will return false
3199 * @param int $userid The id of the user whose submission we want
3200 * @return mixed The group or false
3202 public function get_submission_group($userid) {
3204 if (isset($this->usersubmissiongroups[$userid])) {
3205 return $this->usersubmissiongroups[$userid];
3208 $groups = $this->get_all_groups($userid);
3209 if (count($groups) != 1) {
3210 $return = false;
3211 } else {
3212 $return = array_pop($groups);
3215 // Cache the user submission group.
3216 $this->usersubmissiongroups[$userid] = $return;
3218 return $return;
3222 * Gets all groups the user is a member of.
3224 * @param int $userid Teh id of the user who's groups we are checking
3225 * @return array The group objects
3227 public function get_all_groups($userid) {
3228 if (isset($this->usergroups[$userid])) {
3229 return $this->usergroups[$userid];
3232 $grouping = $this->get_instance()->teamsubmissiongroupingid;
3233 $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping);
3235 $this->usergroups[$userid] = $return;
3237 return $return;
3242 * Display the submission that is used by a plugin.
3244 * Uses url parameters 'sid', 'gid' and 'plugin'.
3246 * @param string $pluginsubtype
3247 * @return string
3249 protected function view_plugin_content($pluginsubtype) {
3250 $o = '';
3252 $submissionid = optional_param('sid', 0, PARAM_INT);
3253 $gradeid = optional_param('gid', 0, PARAM_INT);
3254 $plugintype = required_param('plugin', PARAM_PLUGIN);
3255 $item = null;
3256 if ($pluginsubtype == 'assignsubmission') {
3257 $plugin = $this->get_submission_plugin_by_type($plugintype);
3258 if ($submissionid <= 0) {
3259 throw new coding_exception('Submission id should not be 0');
3261 $item = $this->get_submission($submissionid);
3263 // Check permissions.
3264 if (empty($item->userid)) {
3265 // Group submission.
3266 $this->require_view_group_submission($item->groupid);
3267 } else {
3268 $this->require_view_submission($item->userid);
3270 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3271 $this->get_context(),
3272 $this->show_intro(),
3273 $this->get_course_module()->id,
3274 $plugin->get_name()));
3275 $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin,
3276 $item,
3277 assign_submission_plugin_submission::FULL,
3278 $this->get_course_module()->id,
3279 $this->get_return_action(),
3280 $this->get_return_params()));
3282 // Trigger event for viewing a submission.
3283 \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger();
3285 } else {
3286 $plugin = $this->get_feedback_plugin_by_type($plugintype);
3287 if ($gradeid <= 0) {
3288 throw new coding_exception('Grade id should not be 0');
3290 $item = $this->get_grade($gradeid);
3291 // Check permissions.
3292 $this->require_view_submission($item->userid);
3293 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3294 $this->get_context(),
3295 $this->show_intro(),
3296 $this->get_course_module()->id,
3297 $plugin->get_name()));
3298 $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin,
3299 $item,
3300 assign_feedback_plugin_feedback::FULL,
3301 $this->get_course_module()->id,
3302 $this->get_return_action(),
3303 $this->get_return_params()));
3305 // Trigger event for viewing feedback.
3306 \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger();
3309 $o .= $this->view_return_links();
3311 $o .= $this->view_footer();
3313 return $o;
3317 * Rewrite plugin file urls so they resolve correctly in an exported zip.
3319 * @param string $text - The replacement text
3320 * @param stdClass $user - The user record
3321 * @param assign_plugin $plugin - The assignment plugin
3323 public function download_rewrite_pluginfile_urls($text, $user, $plugin) {
3324 // The groupname prefix for the urls doesn't depend on the group mode of the assignment instance.
3325 // Rather, it should be determined by checking the group submission settings of the instance,
3326 // which is what download_submission() does when generating the file name prefixes.
3327 $groupname = '';
3328 if ($this->get_instance()->teamsubmission) {
3329 $submissiongroup = $this->get_submission_group($user->id);
3330 if ($submissiongroup) {
3331 $groupname = $submissiongroup->name . '-';
3332 } else {
3333 $groupname = get_string('defaultteam', 'assign') . '-';
3337 if ($this->is_blind_marking()) {
3338 $prefix = $groupname . get_string('participant', 'assign');
3339 $prefix = str_replace('_', ' ', $prefix);
3340 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3341 } else {
3342 $prefix = $groupname . fullname($user);
3343 $prefix = str_replace('_', ' ', $prefix);
3344 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3347 // Only prefix files if downloadasfolders user preference is NOT set.
3348 if (!get_user_preferences('assign_downloadasfolders', 1)) {
3349 $subtype = $plugin->get_subtype();
3350 $type = $plugin->get_type();
3351 $prefix = $prefix . $subtype . '_' . $type . '_';
3352 } else {
3353 $prefix = "";
3355 $result = str_replace('@@PLUGINFILE@@/', $prefix, $text);
3357 return $result;
3361 * Render the content in editor that is often used by plugin.
3363 * @param string $filearea
3364 * @param int $submissionid
3365 * @param string $plugintype
3366 * @param string $editor
3367 * @param string $component
3368 * @param bool $shortentext Whether to shorten the text content.
3369 * @return string
3371 public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component, $shortentext = false) {
3372 global $CFG;
3374 $result = '';
3376 $plugin = $this->get_submission_plugin_by_type($plugintype);
3378 $text = $plugin->get_editor_text($editor, $submissionid);
3379 if ($shortentext) {
3380 $text = shorten_text($text, 140);
3382 $format = $plugin->get_editor_format($editor, $submissionid);
3384 $finaltext = file_rewrite_pluginfile_urls($text,
3385 'pluginfile.php',
3386 $this->get_context()->id,
3387 $component,
3388 $filearea,
3389 $submissionid);
3390 $params = array('overflowdiv' => true, 'context' => $this->get_context());
3391 $result .= format_text($finaltext, $format, $params);
3393 if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) {
3394 require_once($CFG->libdir . '/portfoliolib.php');
3396 $button = new portfolio_add_button();
3397 $portfolioparams = array('cmid' => $this->get_course_module()->id,
3398 'sid' => $submissionid,
3399 'plugin' => $plugintype,
3400 'editor' => $editor,
3401 'area'=>$filearea);
3402 $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign');
3403 $fs = get_file_storage();
3405 if ($files = $fs->get_area_files($this->context->id,
3406 $component,
3407 $filearea,
3408 $submissionid,
3409 'timemodified',
3410 false)) {
3411 $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3412 } else {
3413 $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
3415 $result .= $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
3417 return $result;
3421 * Display a continue page after grading.
3423 * @param string $message - The message to display.
3424 * @return string
3426 protected function view_savegrading_result($message) {
3427 $o = '';
3428 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3429 $this->get_context(),
3430 $this->show_intro(),
3431 $this->get_course_module()->id,
3432 get_string('savegradingresult', 'assign')));
3433 $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'),
3434 $message,
3435 $this->get_course_module()->id);
3436 $o .= $this->get_renderer()->render($gradingresult);
3437 $o .= $this->view_footer();
3438 return $o;
3441 * Display a continue page after quickgrading.
3443 * @param string $message - The message to display.
3444 * @return string
3446 protected function view_quickgrading_result($message) {
3447 $o = '';
3448 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3449 $this->get_context(),
3450 $this->show_intro(),
3451 $this->get_course_module()->id,
3452 get_string('quickgradingresult', 'assign')));
3453 $gradingerror = in_array($message, $this->get_error_messages());
3454 $lastpage = optional_param('lastpage', null, PARAM_INT);
3455 $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'),
3456 $message,
3457 $this->get_course_module()->id,
3458 $gradingerror,
3459 $lastpage);
3460 $o .= $this->get_renderer()->render($gradingresult);
3461 $o .= $this->view_footer();
3462 return $o;
3466 * Display the page footer.
3468 * @return string
3470 protected function view_footer() {
3471 // When viewing the footer during PHPUNIT tests a set_state error is thrown.
3472 if (!PHPUNIT_TEST) {
3473 return $this->get_renderer()->render_footer();
3476 return '';
3480 * Throw an error if the permissions to view this users' group submission are missing.
3482 * @param int $groupid Group id.
3483 * @throws required_capability_exception
3485 public function require_view_group_submission($groupid) {
3486 if (!$this->can_view_group_submission($groupid)) {
3487 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3492 * Throw an error if the permissions to view this users submission are missing.
3494 * @throws required_capability_exception
3495 * @return none
3497 public function require_view_submission($userid) {
3498 if (!$this->can_view_submission($userid)) {
3499 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3504 * Throw an error if the permissions to view grades in this assignment are missing.
3506 * @throws required_capability_exception
3507 * @return none
3509 public function require_view_grades() {
3510 if (!$this->can_view_grades()) {
3511 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3516 * Does this user have view grade or grade permission for this assignment?
3518 * @param mixed $groupid int|null when is set to a value, use this group instead calculating it
3519 * @return bool
3521 public function can_view_grades($groupid = null) {
3522 // Permissions check.
3523 if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
3524 return false;
3526 // Checks for the edge case when user belongs to no groups and groupmode is sep.
3527 if ($this->get_course_module()->effectivegroupmode == SEPARATEGROUPS) {
3528 if ($groupid === null) {
3529 $groupid = groups_get_activity_allowed_groups($this->get_course_module());
3531 $groupflag = has_capability('moodle/site:accessallgroups', $this->get_context());
3532 $groupflag = $groupflag || !empty($groupid);
3533 return (bool)$groupflag;
3535 return true;
3539 * Does this user have grade permission for this assignment?
3541 * @param int|stdClass $user The object or id of the user who will do the editing (default to current user).
3542 * @return bool
3544 public function can_grade($user = null) {
3545 // Permissions check.
3546 if (!has_capability('mod/assign:grade', $this->context, $user)) {
3547 return false;
3550 return true;
3554 * Download a zip file of all assignment submissions.
3556 * @param array $userids Array of user ids to download assignment submissions in a zip file
3557 * @return string - If an error occurs, this will contain the error page.
3559 protected function download_submissions($userids = false) {
3560 global $CFG, $DB;
3562 // More efficient to load this here.
3563 require_once($CFG->libdir.'/filelib.php');
3565 // Increase the server timeout to handle the creation and sending of large zip files.
3566 core_php_time_limit::raise();
3568 $this->require_view_grades();
3570 // Load all users with submit.
3571 $students = get_enrolled_users($this->context, "mod/assign:submit", null, 'u.*', null, null, null,
3572 $this->show_only_active_users());
3574 // Build a list of files to zip.
3575 $filesforzipping = array();
3576 $fs = get_file_storage();
3578 $groupmode = groups_get_activity_groupmode($this->get_course_module());
3579 // All users.
3580 $groupid = 0;
3581 $groupname = '';
3582 if ($groupmode) {
3583 $groupid = groups_get_activity_group($this->get_course_module(), true);
3584 if (!empty($groupid)) {
3585 $groupname = groups_get_group_name($groupid) . '-';
3589 // Construct the zip file name.
3590 $filename = clean_filename($this->get_course()->shortname . '-' .
3591 $this->get_instance()->name . '-' .
3592 $groupname.$this->get_course_module()->id . '.zip');
3594 // Get all the files for each student.
3595 foreach ($students as $student) {
3596 $userid = $student->id;
3597 // Download all assigments submission or only selected users.
3598 if ($userids and !in_array($userid, $userids)) {
3599 continue;
3602 if ((groups_is_member($groupid, $userid) or !$groupmode or !$groupid)) {
3603 // Get the plugins to add their own files to the zip.
3605 $submissiongroup = false;
3606 $groupname = '';
3607 if ($this->get_instance()->teamsubmission) {
3608 $submission = $this->get_group_submission($userid, 0, false);
3609 $submissiongroup = $this->get_submission_group($userid);
3610 if ($submissiongroup) {
3611 $groupname = $submissiongroup->name . '-';
3612 } else {
3613 $groupname = get_string('defaultteam', 'assign') . '-';
3615 } else {
3616 $submission = $this->get_user_submission($userid, false);
3619 if ($this->is_blind_marking()) {
3620 $prefix = str_replace('_', ' ', $groupname . get_string('participant', 'assign'));
3621 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
3622 } else {
3623 $fullname = fullname($student, has_capability('moodle/site:viewfullnames', $this->get_context()));
3624 $prefix = str_replace('_', ' ', $groupname . $fullname);
3625 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
3628 if ($submission) {
3629 $downloadasfolders = get_user_preferences('assign_downloadasfolders', 1);
3630 foreach ($this->submissionplugins as $plugin) {
3631 if ($plugin->is_enabled() && $plugin->is_visible()) {
3632 if ($downloadasfolders) {
3633 // Create a folder for each user for each assignment plugin.
3634 // This is the default behavior for version of Moodle >= 3.1.
3635 $submission->exportfullpath = true;
3636 $pluginfiles = $plugin->get_files($submission, $student);
3637 foreach ($pluginfiles as $zipfilepath => $file) {
3638 $subtype = $plugin->get_subtype();
3639 $type = $plugin->get_type();
3640 $zipfilename = basename($zipfilepath);
3641 $prefixedfilename = clean_filename($prefix .
3642 '_' .
3643 $subtype .
3644 '_' .
3645 $type .
3646 '_');
3647 if ($type == 'file') {
3648 $pathfilename = $prefixedfilename . $file->get_filepath() . $zipfilename;
3649 } else if ($type == 'onlinetext') {
3650 $pathfilename = $prefixedfilename . '/' . $zipfilename;
3651 } else {
3652 $pathfilename = $prefixedfilename . '/' . $zipfilename;
3654 $pathfilename = clean_param($pathfilename, PARAM_PATH);
3655 $filesforzipping[$pathfilename] = $file;
3657 } else {
3658 // Create a single folder for all users of all assignment plugins.
3659 // This was the default behavior for version of Moodle < 3.1.
3660 $submission->exportfullpath = false;
3661 $pluginfiles = $plugin->get_files($submission, $student);
3662 foreach ($pluginfiles as $zipfilename => $file) {
3663 $subtype = $plugin->get_subtype();
3664 $type = $plugin->get_type();
3665 $prefixedfilename = clean_filename($prefix .
3666 '_' .
3667 $subtype .
3668 '_' .
3669 $type .
3670 '_' .
3671 $zipfilename);
3672 $filesforzipping[$prefixedfilename] = $file;
3680 $result = '';
3681 if (count($filesforzipping) == 0) {
3682 $header = new assign_header($this->get_instance(),
3683 $this->get_context(),
3685 $this->get_course_module()->id,
3686 get_string('downloadall', 'assign'));
3687 $result .= $this->get_renderer()->render($header);
3688 $result .= $this->get_renderer()->notification(get_string('nosubmission', 'assign'));
3689 $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id,
3690 'action'=>'grading'));
3691 $result .= $this->get_renderer()->continue_button($url);
3692 $result .= $this->view_footer();
3694 return $result;
3697 // Log zip as downloaded.
3698 \mod_assign\event\all_submissions_downloaded::create_from_assign($this)->trigger();
3700 // Close the session.
3701 \core\session\manager::write_close();
3703 $zipwriter = \core_files\archive_writer::get_stream_writer($filename, \core_files\archive_writer::ZIP_WRITER);
3705 // Stream the files into the zip.
3706 foreach ($filesforzipping as $pathinzip => $file) {
3707 if ($file instanceof \stored_file) {
3708 // Most of cases are \stored_file.
3709 $zipwriter->add_file_from_stored_file($pathinzip, $file);
3710 } else if (is_array($file)) {
3711 // Save $file as contents, from onlinetext subplugin.
3712 $content = reset($file);
3713 $zipwriter->add_file_from_string($pathinzip, $content);
3717 // Finish the archive.
3718 $zipwriter->finish();
3719 exit();
3723 * Util function to add a message to the log.
3725 * @deprecated since 2.7 - Use new events system instead.
3726 * (see http://docs.moodle.org/dev/Migrating_logging_calls_in_plugins).
3728 * @param string $action The current action
3729 * @param string $info A detailed description of the change. But no more than 255 characters.
3730 * @param string $url The url to the assign module instance.
3731 * @param bool $return If true, returns the arguments, else adds to log. The purpose of this is to
3732 * retrieve the arguments to use them with the new event system (Event 2).
3733 * @return void|array
3735 public function add_to_log($action = '', $info = '', $url='', $return = false) {
3736 global $USER;
3738 $fullurl = 'view.php?id=' . $this->get_course_module()->id;
3739 if ($url != '') {
3740 $fullurl .= '&' . $url;
3743 $args = array(
3744 $this->get_course()->id,
3745 'assign',
3746 $action,
3747 $fullurl,
3748 $info,
3749 $this->get_course_module()->id
3752 if ($return) {
3753 // We only need to call debugging when returning a value. This is because the call to
3754 // call_user_func_array('add_to_log', $args) will trigger a debugging message of it's own.
3755 debugging('The mod_assign add_to_log() function is now deprecated.', DEBUG_DEVELOPER);
3756 return $args;
3758 call_user_func_array('add_to_log', $args);
3762 * Lazy load the page renderer and expose the renderer to plugins.
3764 * @return assign_renderer
3766 public function get_renderer() {
3767 global $PAGE;
3768 if ($this->output) {
3769 return $this->output;
3771 $this->output = $PAGE->get_renderer('mod_assign', null, RENDERER_TARGET_GENERAL);
3772 return $this->output;
3776 * Load the submission object for a particular user, optionally creating it if required.
3778 * For team assignments there are 2 submissions - the student submission and the team submission
3779 * All files are associated with the team submission but the status of the students contribution is
3780 * recorded separately.
3782 * @param int $userid The id of the user whose submission we want or 0 in which case USER->id is used
3783 * @param bool $create If set to true a new submission object will be created in the database with the status set to "new".
3784 * @param int $attemptnumber - -1 means the latest attempt
3785 * @return stdClass The submission
3787 public function get_user_submission($userid, $create, $attemptnumber=-1) {
3788 global $DB, $USER;
3790 if (!$userid) {
3791 $userid = $USER->id;
3793 // If the userid is not null then use userid.
3794 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
3795 if ($attemptnumber >= 0) {
3796 $params['attemptnumber'] = $attemptnumber;
3799 // Only return the row with the highest attemptnumber.
3800 $submission = null;
3801 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
3802 if ($submissions) {
3803 $submission = reset($submissions);
3806 if ($submission) {
3807 return $submission;
3809 if ($create) {
3810 $submission = new stdClass();
3811 $submission->assignment = $this->get_instance()->id;
3812 $submission->userid = $userid;
3813 $submission->timecreated = time();
3814 $submission->timemodified = $submission->timecreated;
3815 $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
3816 if ($attemptnumber >= 0) {
3817 $submission->attemptnumber = $attemptnumber;
3818 } else {
3819 $submission->attemptnumber = 0;
3821 // Work out if this is the latest submission.
3822 $submission->latest = 0;
3823 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
3824 if ($attemptnumber == -1) {
3825 // This is a new submission so it must be the latest.
3826 $submission->latest = 1;
3827 } else {
3828 // We need to work this out.
3829 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
3830 $latestsubmission = null;
3831 if ($result) {
3832 $latestsubmission = reset($result);
3834 if (empty($latestsubmission) || ($attemptnumber > $latestsubmission->attemptnumber)) {
3835 $submission->latest = 1;
3838 if ($submission->latest) {
3839 // This is the case when we need to set latest to 0 for all the other attempts.
3840 $DB->set_field('assign_submission', 'latest', 0, $params);
3842 $sid = $DB->insert_record('assign_submission', $submission);
3843 return $DB->get_record('assign_submission', array('id' => $sid));
3845 return false;
3849 * Load the submission object from it's id.
3851 * @param int $submissionid The id of the submission we want
3852 * @return stdClass The submission
3854 protected function get_submission($submissionid) {
3855 global $DB;
3857 $params = array('assignment'=>$this->get_instance()->id, 'id'=>$submissionid);
3858 return $DB->get_record('assign_submission', $params, '*', MUST_EXIST);
3862 * This will retrieve a user flags object from the db optionally creating it if required.
3863 * The user flags was split from the user_grades table in 2.5.
3865 * @param int $userid The user we are getting the flags for.
3866 * @param bool $create If true the flags record will be created if it does not exist
3867 * @return stdClass The flags record
3869 public function get_user_flags($userid, $create) {
3870 global $DB, $USER;
3872 // If the userid is not null then use userid.
3873 if (!$userid) {
3874 $userid = $USER->id;
3877 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
3879 $flags = $DB->get_record('assign_user_flags', $params);
3881 if ($flags) {
3882 return $flags;
3884 if ($create) {
3885 $flags = new stdClass();
3886 $flags->assignment = $this->get_instance()->id;
3887 $flags->userid = $userid;
3888 $flags->locked = 0;
3889 $flags->extensionduedate = 0;
3890 $flags->workflowstate = '';
3891 $flags->allocatedmarker = 0;
3893 // The mailed flag can be one of 3 values: 0 is unsent, 1 is sent and 2 is do not send yet.
3894 // This is because students only want to be notified about certain types of update (grades and feedback).
3895 $flags->mailed = 2;
3897 $fid = $DB->insert_record('assign_user_flags', $flags);
3898 $flags->id = $fid;
3899 return $flags;
3901 return false;
3905 * This will retrieve a grade object from the db, optionally creating it if required.
3907 * @param int $userid The user we are grading
3908 * @param bool $create If true the grade will be created if it does not exist
3909 * @param int $attemptnumber The attempt number to retrieve the grade for. -1 means the latest submission.
3910 * @return stdClass The grade record
3912 public function get_user_grade($userid, $create, $attemptnumber=-1) {
3913 global $DB, $USER;
3915 // If the userid is not null then use userid.
3916 if (!$userid) {
3917 $userid = $USER->id;
3919 $submission = null;
3921 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
3922 if ($attemptnumber < 0 || $create) {
3923 // Make sure this grade matches the latest submission attempt.
3924 if ($this->get_instance()->teamsubmission) {
3925 $submission = $this->get_group_submission($userid, 0, true, $attemptnumber);
3926 } else {
3927 $submission = $this->get_user_submission($userid, true, $attemptnumber);
3929 if ($submission) {
3930 $attemptnumber = $submission->attemptnumber;
3934 if ($attemptnumber >= 0) {
3935 $params['attemptnumber'] = $attemptnumber;
3938 $grades = $DB->get_records('assign_grades', $params, 'attemptnumber DESC', '*', 0, 1);
3940 if ($grades) {
3941 return reset($grades);
3943 if ($create) {
3944 $grade = new stdClass();
3945 $grade->assignment = $this->get_instance()->id;
3946 $grade->userid = $userid;
3947 $grade->timecreated = time();
3948 // If we are "auto-creating" a grade - and there is a submission
3949 // the new grade should not have a more recent timemodified value
3950 // than the submission.
3951 if ($submission) {
3952 $grade->timemodified = $submission->timemodified;
3953 } else {
3954 $grade->timemodified = $grade->timecreated;
3956 $grade->grade = -1;
3957 // Do not set the grader id here as it would be the admin users which is incorrect.
3958 $grade->grader = -1;
3959 if ($attemptnumber >= 0) {
3960 $grade->attemptnumber = $attemptnumber;
3963 $gid = $DB->insert_record('assign_grades', $grade);
3964 $grade->id = $gid;
3965 return $grade;
3967 return false;
3971 * This will retrieve a grade object from the db.
3973 * @param int $gradeid The id of the grade
3974 * @return stdClass The grade record
3976 protected function get_grade($gradeid) {
3977 global $DB;
3979 $params = array('assignment'=>$this->get_instance()->id, 'id'=>$gradeid);
3980 return $DB->get_record('assign_grades', $params, '*', MUST_EXIST);
3984 * Print the grading page for a single user submission.
3986 * @param array $args Optional args array (better than pulling args from _GET and _POST)
3987 * @return string
3989 protected function view_single_grading_panel($args) {
3990 global $DB, $CFG;
3992 $o = '';
3994 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
3996 // Need submit permission to submit an assignment.
3997 require_capability('mod/assign:grade', $this->context);
3999 // If userid is passed - we are only grading a single student.
4000 $userid = $args['userid'];
4001 $attemptnumber = $args['attemptnumber'];
4002 $instance = $this->get_instance($userid);
4004 // Apply overrides.
4005 $this->update_effective_access($userid);
4007 $rownum = 0;
4008 $useridlist = array($userid);
4010 $last = true;
4011 // This variation on the url will link direct to this student, with no next/previous links.
4012 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4013 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4014 $this->register_return_link('grade', $returnparams);
4016 $user = $DB->get_record('user', array('id' => $userid));
4017 $submission = $this->get_user_submission($userid, false, $attemptnumber);
4018 $submissiongroup = null;
4019 $teamsubmission = null;
4020 $notsubmitted = array();
4021 if ($instance->teamsubmission) {
4022 $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
4023 $submissiongroup = $this->get_submission_group($userid);
4024 $groupid = 0;
4025 if ($submissiongroup) {
4026 $groupid = $submissiongroup->id;
4028 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
4032 // Get the requested grade.
4033 $grade = $this->get_user_grade($userid, false, $attemptnumber);
4034 $flags = $this->get_user_flags($userid, false);
4035 if ($this->can_view_submission($userid)) {
4036 $submissionlocked = ($flags && $flags->locked);
4037 $extensionduedate = null;
4038 if ($flags) {
4039 $extensionduedate = $flags->extensionduedate;
4041 $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
4042 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4043 $usergroups = $this->get_all_groups($user->id);
4045 $submissionstatus = new assign_submission_status_compact($instance->allowsubmissionsfromdate,
4046 $instance->alwaysshowdescription,
4047 $submission,
4048 $instance->teamsubmission,
4049 $teamsubmission,
4050 $submissiongroup,
4051 $notsubmitted,
4052 $this->is_any_submission_plugin_enabled(),
4053 $submissionlocked,
4054 $this->is_graded($userid),
4055 $instance->duedate,
4056 $instance->cutoffdate,
4057 $this->get_submission_plugins(),
4058 $this->get_return_action(),
4059 $this->get_return_params(),
4060 $this->get_course_module()->id,
4061 $this->get_course()->id,
4062 assign_submission_status::GRADER_VIEW,
4063 $showedit,
4064 false,
4065 $viewfullnames,
4066 $extensionduedate,
4067 $this->get_context(),
4068 $this->is_blind_marking(),
4070 $instance->attemptreopenmethod,
4071 $instance->maxattempts,
4072 $this->get_grading_status($userid),
4073 $instance->preventsubmissionnotingroup,
4074 $usergroups);
4075 $o .= $this->get_renderer()->render($submissionstatus);
4078 if ($grade) {
4079 $data = new stdClass();
4080 if ($grade->grade !== null && $grade->grade >= 0) {
4081 $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
4083 } else {
4084 $data = new stdClass();
4085 $data->grade = '';
4088 if (!empty($flags->workflowstate)) {
4089 $data->workflowstate = $flags->workflowstate;
4091 if (!empty($flags->allocatedmarker)) {
4092 $data->allocatedmarker = $flags->allocatedmarker;
4095 // Warning if required.
4096 $allsubmissions = $this->get_all_submissions($userid);
4098 if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
4099 $params = array('attemptnumber' => $attemptnumber + 1,
4100 'totalattempts' => count($allsubmissions));
4101 $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
4102 $o .= $this->get_renderer()->notification($message);
4105 $pagination = array('rownum' => $rownum,
4106 'useridlistid' => 0,
4107 'last' => $last,
4108 'userid' => $userid,
4109 'attemptnumber' => $attemptnumber,
4110 'gradingpanel' => true);
4112 if (!empty($args['formdata'])) {
4113 $data = (array) $data;
4114 $data = (object) array_merge($data, $args['formdata']);
4116 $formparams = array($this, $data, $pagination);
4117 $mform = new mod_assign_grade_form(null,
4118 $formparams,
4119 'post',
4121 array('class' => 'gradeform'));
4123 if (!empty($args['formdata'])) {
4124 // If we were passed form data - we want the form to check the data
4125 // and show errors.
4126 $mform->is_validated();
4128 $o .= $this->get_renderer()->heading(get_string('gradenoun'), 3);
4129 $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
4131 if (count($allsubmissions) > 1) {
4132 $allgrades = $this->get_all_grades($userid);
4133 $history = new assign_attempt_history_chooser($allsubmissions,
4134 $allgrades,
4135 $this->get_course_module()->id,
4136 $userid);
4138 $o .= $this->get_renderer()->render($history);
4141 \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
4143 return $o;
4147 * Print the grading page for a single user submission.
4149 * @param moodleform $mform
4150 * @return string
4152 protected function view_single_grade_page($mform) {
4153 global $DB, $CFG, $SESSION;
4155 $o = '';
4156 $instance = $this->get_instance();
4158 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
4160 // Need submit permission to submit an assignment.
4161 require_capability('mod/assign:grade', $this->context);
4163 $header = new assign_header($instance,
4164 $this->get_context(),
4165 false,
4166 $this->get_course_module()->id,
4167 get_string('grading', 'assign'));
4168 $o .= $this->get_renderer()->render($header);
4170 // If userid is passed - we are only grading a single student.
4171 $rownum = optional_param('rownum', 0, PARAM_INT);
4172 $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
4173 $userid = optional_param('userid', 0, PARAM_INT);
4174 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
4176 if (!$userid) {
4177 $useridlist = $this->get_grading_userid_list(true, $useridlistid);
4178 } else {
4179 $rownum = 0;
4180 $useridlistid = 0;
4181 $useridlist = array($userid);
4184 if ($rownum < 0 || $rownum > count($useridlist)) {
4185 throw new coding_exception('Row is out of bounds for the current grading table: ' . $rownum);
4188 $last = false;
4189 $userid = $useridlist[$rownum];
4190 if ($rownum == count($useridlist) - 1) {
4191 $last = true;
4193 // This variation on the url will link direct to this student, with no next/previous links.
4194 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4195 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4196 $this->register_return_link('grade', $returnparams);
4198 $user = $DB->get_record('user', array('id' => $userid));
4199 if ($user) {
4200 $this->update_effective_access($userid);
4201 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4202 $usersummary = new assign_user_summary($user,
4203 $this->get_course()->id,
4204 $viewfullnames,
4205 $this->is_blind_marking(),
4206 $this->get_uniqueid_for_user($user->id),
4207 // TODO Does not support custom user profile fields (MDL-70456).
4208 \core_user\fields::get_identity_fields($this->get_context(), false),
4209 !$this->is_active_user($userid));
4210 $o .= $this->get_renderer()->render($usersummary);
4212 $submission = $this->get_user_submission($userid, false, $attemptnumber);
4213 $submissiongroup = null;
4214 $teamsubmission = null;
4215 $notsubmitted = array();
4216 if ($instance->teamsubmission) {
4217 $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
4218 $submissiongroup = $this->get_submission_group($userid);
4219 $groupid = 0;
4220 if ($submissiongroup) {
4221 $groupid = $submissiongroup->id;
4223 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
4227 // Get the requested grade.
4228 $grade = $this->get_user_grade($userid, false, $attemptnumber);
4229 $flags = $this->get_user_flags($userid, false);
4230 if ($this->can_view_submission($userid)) {
4231 $submissionlocked = ($flags && $flags->locked);
4232 $extensionduedate = null;
4233 if ($flags) {
4234 $extensionduedate = $flags->extensionduedate;
4236 $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
4237 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4238 $usergroups = $this->get_all_groups($user->id);
4240 $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
4241 $instance->alwaysshowdescription,
4242 $submission,
4243 $instance->teamsubmission,
4244 $teamsubmission,
4245 $submissiongroup,
4246 $notsubmitted,
4247 $this->is_any_submission_plugin_enabled(),
4248 $submissionlocked,
4249 $this->is_graded($userid),
4250 $instance->duedate,
4251 $instance->cutoffdate,
4252 $this->get_submission_plugins(),
4253 $this->get_return_action(),
4254 $this->get_return_params(),
4255 $this->get_course_module()->id,
4256 $this->get_course()->id,
4257 assign_submission_status::GRADER_VIEW,
4258 $showedit,
4259 false,
4260 $viewfullnames,
4261 $extensionduedate,
4262 $this->get_context(),
4263 $this->is_blind_marking(),
4265 $instance->attemptreopenmethod,
4266 $instance->maxattempts,
4267 $this->get_grading_status($userid),
4268 $instance->preventsubmissionnotingroup,
4269 $usergroups);
4270 $o .= $this->get_renderer()->render($submissionstatus);
4273 if ($grade) {
4274 $data = new stdClass();
4275 if ($grade->grade !== null && $grade->grade >= 0) {
4276 $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
4278 } else {
4279 $data = new stdClass();
4280 $data->grade = '';
4283 if (!empty($flags->workflowstate)) {
4284 $data->workflowstate = $flags->workflowstate;
4286 if (!empty($flags->allocatedmarker)) {
4287 $data->allocatedmarker = $flags->allocatedmarker;
4290 // Warning if required.
4291 $allsubmissions = $this->get_all_submissions($userid);
4293 if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
4294 $params = array('attemptnumber'=>$attemptnumber + 1,
4295 'totalattempts'=>count($allsubmissions));
4296 $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
4297 $o .= $this->get_renderer()->notification($message);
4300 // Now show the grading form.
4301 if (!$mform) {
4302 $pagination = array('rownum' => $rownum,
4303 'useridlistid' => $useridlistid,
4304 'last' => $last,
4305 'userid' => $userid,
4306 'attemptnumber' => $attemptnumber);
4307 $formparams = array($this, $data, $pagination);
4308 $mform = new mod_assign_grade_form(null,
4309 $formparams,
4310 'post',
4312 array('class'=>'gradeform'));
4314 $o .= $this->get_renderer()->heading(get_string('gradenoun'), 3);
4315 $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
4317 if (count($allsubmissions) > 1 && $attemptnumber == -1) {
4318 $allgrades = $this->get_all_grades($userid);
4319 $history = new assign_attempt_history($allsubmissions,
4320 $allgrades,
4321 $this->get_submission_plugins(),
4322 $this->get_feedback_plugins(),
4323 $this->get_course_module()->id,
4324 $this->get_return_action(),
4325 $this->get_return_params(),
4326 true,
4327 $useridlistid,
4328 $rownum);
4330 $o .= $this->get_renderer()->render($history);
4333 \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
4335 $o .= $this->view_footer();
4336 return $o;
4340 * Show a confirmation page to make sure they want to remove submission data.
4342 * @return string
4344 protected function view_remove_submission_confirm() {
4345 global $USER, $DB;
4347 $userid = optional_param('userid', $USER->id, PARAM_INT);
4349 if (!$this->can_edit_submission($userid, $USER->id)) {
4350 print_error('nopermission');
4352 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
4354 $o = '';
4355 $header = new assign_header($this->get_instance(),
4356 $this->get_context(),
4357 false,
4358 $this->get_course_module()->id);
4359 $o .= $this->get_renderer()->render($header);
4361 $urlparams = array('id' => $this->get_course_module()->id,
4362 'action' => 'removesubmission',
4363 'userid' => $userid,
4364 'sesskey' => sesskey());
4365 $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
4367 $urlparams = array('id' => $this->get_course_module()->id,
4368 'action' => 'view');
4369 $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
4371 if ($userid == $USER->id) {
4372 $confirmstr = get_string('removesubmissionconfirm', 'assign');
4373 } else {
4374 $name = $this->fullname($user);
4375 $confirmstr = get_string('removesubmissionconfirmforstudent', 'assign', $name);
4377 $o .= $this->get_renderer()->confirm($confirmstr,
4378 $confirmurl,
4379 $cancelurl);
4380 $o .= $this->view_footer();
4382 \mod_assign\event\remove_submission_form_viewed::create_from_user($this, $user)->trigger();
4384 return $o;
4389 * Show a confirmation page to make sure they want to release student identities.
4391 * @return string
4393 protected function view_reveal_identities_confirm() {
4394 require_capability('mod/assign:revealidentities', $this->get_context());
4396 $o = '';
4397 $header = new assign_header($this->get_instance(),
4398 $this->get_context(),
4399 false,
4400 $this->get_course_module()->id);
4401 $o .= $this->get_renderer()->render($header);
4403 $urlparams = array('id'=>$this->get_course_module()->id,
4404 'action'=>'revealidentitiesconfirm',
4405 'sesskey'=>sesskey());
4406 $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
4408 $urlparams = array('id'=>$this->get_course_module()->id,
4409 'action'=>'grading');
4410 $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
4412 $o .= $this->get_renderer()->confirm(get_string('revealidentitiesconfirm', 'assign'),
4413 $confirmurl,
4414 $cancelurl);
4415 $o .= $this->view_footer();
4417 \mod_assign\event\reveal_identities_confirmation_page_viewed::create_from_assign($this)->trigger();
4419 return $o;
4423 * View a link to go back to the previous page. Uses url parameters returnaction and returnparams.
4425 * @return string
4427 protected function view_return_links() {
4428 $returnaction = optional_param('returnaction', '', PARAM_ALPHA);
4429 $returnparams = optional_param('returnparams', '', PARAM_TEXT);
4431 $params = array();
4432 $returnparams = str_replace('&amp;', '&', $returnparams);
4433 parse_str($returnparams, $params);
4434 $newparams = array('id' => $this->get_course_module()->id, 'action' => $returnaction);
4435 $params = array_merge($newparams, $params);
4437 $url = new moodle_url('/mod/assign/view.php', $params);
4438 return $this->get_renderer()->single_button($url, get_string('back'), 'get');
4442 * View the grading table of all submissions for this assignment.
4444 * @return string
4446 protected function view_grading_table() {
4447 global $USER, $CFG, $SESSION;
4449 // Include grading options form.
4450 require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
4451 require_once($CFG->dirroot . '/mod/assign/quickgradingform.php');
4452 require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
4453 $o = '';
4454 $cmid = $this->get_course_module()->id;
4456 $links = array();
4457 if (has_capability('gradereport/grader:view', $this->get_course_context()) &&
4458 has_capability('moodle/grade:viewall', $this->get_course_context())) {
4459 $gradebookurl = '/grade/report/grader/index.php?id=' . $this->get_course()->id;
4460 $links[$gradebookurl] = get_string('viewgradebook', 'assign');
4462 if ($this->is_any_submission_plugin_enabled() && $this->count_submissions()) {
4463 $downloadurl = '/mod/assign/view.php?id=' . $cmid . '&action=downloadall';
4464 $links[$downloadurl] = get_string('downloadall', 'assign');
4466 if ($this->is_blind_marking() &&
4467 has_capability('mod/assign:revealidentities', $this->get_context())) {
4468 $revealidentitiesurl = '/mod/assign/view.php?id=' . $cmid . '&action=revealidentities';
4469 $links[$revealidentitiesurl] = get_string('revealidentities', 'assign');
4471 foreach ($this->get_feedback_plugins() as $plugin) {
4472 if ($plugin->is_enabled() && $plugin->is_visible()) {
4473 foreach ($plugin->get_grading_actions() as $action => $description) {
4474 $url = '/mod/assign/view.php' .
4475 '?id=' . $cmid .
4476 '&plugin=' . $plugin->get_type() .
4477 '&pluginsubtype=assignfeedback' .
4478 '&action=viewpluginpage&pluginaction=' . $action;
4479 $links[$url] = $description;
4484 // Sort links alphabetically based on the link description.
4485 core_collator::asort($links);
4487 $gradingactions = new url_select($links);
4488 $gradingactions->set_label(get_string('choosegradingaction', 'assign'));
4490 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
4492 $perpage = $this->get_assign_perpage();
4493 $filter = get_user_preferences('assign_filter', '');
4494 $markerfilter = get_user_preferences('assign_markerfilter', '');
4495 $workflowfilter = get_user_preferences('assign_workflowfilter', '');
4496 $controller = $gradingmanager->get_active_controller();
4497 $showquickgrading = empty($controller) && $this->can_grade();
4498 $quickgrading = get_user_preferences('assign_quickgrading', false);
4499 $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
4500 $downloadasfolders = get_user_preferences('assign_downloadasfolders', 1);
4502 $markingallocation = $this->get_instance()->markingworkflow &&
4503 $this->get_instance()->markingallocation &&
4504 has_capability('mod/assign:manageallocations', $this->context);
4505 // Get markers to use in drop lists.
4506 $markingallocationoptions = array();
4507 if ($markingallocation) {
4508 list($sort, $params) = users_order_by_sql('u');
4509 // Only enrolled users could be assigned as potential markers.
4510 $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
4511 $markingallocationoptions[''] = get_string('filternone', 'assign');
4512 $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
4513 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
4514 foreach ($markers as $marker) {
4515 $markingallocationoptions[$marker->id] = fullname($marker, $viewfullnames);
4519 $markingworkflow = $this->get_instance()->markingworkflow;
4520 // Get marking states to show in form.
4521 $markingworkflowoptions = $this->get_marking_workflow_filters();
4523 // Print options for changing the filter and changing the number of results per page.
4524 $gradingoptionsformparams = array('cm'=>$cmid,
4525 'contextid'=>$this->context->id,
4526 'userid'=>$USER->id,
4527 'submissionsenabled'=>$this->is_any_submission_plugin_enabled(),
4528 'showquickgrading'=>$showquickgrading,
4529 'quickgrading'=>$quickgrading,
4530 'markingworkflowopt'=>$markingworkflowoptions,
4531 'markingallocationopt'=>$markingallocationoptions,
4532 'showonlyactiveenrolopt'=>$showonlyactiveenrolopt,
4533 'showonlyactiveenrol' => $this->show_only_active_users(),
4534 'downloadasfolders' => $downloadasfolders);
4536 $classoptions = array('class'=>'gradingoptionsform');
4537 $gradingoptionsform = new mod_assign_grading_options_form(null,
4538 $gradingoptionsformparams,
4539 'post',
4541 $classoptions);
4543 $batchformparams = array('cm'=>$cmid,
4544 'submissiondrafts'=>$this->get_instance()->submissiondrafts,
4545 'duedate'=>$this->get_instance()->duedate,
4546 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
4547 'feedbackplugins'=>$this->get_feedback_plugins(),
4548 'context'=>$this->get_context(),
4549 'markingworkflow'=>$markingworkflow,
4550 'markingallocation'=>$markingallocation);
4551 $classoptions = array('class'=>'gradingbatchoperationsform');
4553 $gradingbatchoperationsform = new mod_assign_grading_batch_operations_form(null,
4554 $batchformparams,
4555 'post',
4557 $classoptions);
4559 $gradingoptionsdata = new stdClass();
4560 $gradingoptionsdata->perpage = $perpage;
4561 $gradingoptionsdata->filter = $filter;
4562 $gradingoptionsdata->markerfilter = $markerfilter;
4563 $gradingoptionsdata->workflowfilter = $workflowfilter;
4564 $gradingoptionsform->set_data($gradingoptionsdata);
4566 $actionformtext = $this->get_renderer()->render($gradingactions);
4567 $header = new assign_header($this->get_instance(),
4568 $this->get_context(),
4569 false,
4570 $this->get_course_module()->id,
4571 get_string('grading', 'assign'),
4572 $actionformtext);
4573 $o .= $this->get_renderer()->render($header);
4575 $currenturl = $CFG->wwwroot .
4576 '/mod/assign/view.php?id=' .
4577 $this->get_course_module()->id .
4578 '&action=grading';
4580 $o .= groups_print_activity_menu($this->get_course_module(), $currenturl, true);
4582 // Plagiarism update status apearring in the grading book.
4583 if (!empty($CFG->enableplagiarism)) {
4584 require_once($CFG->libdir . '/plagiarismlib.php');
4585 $o .= plagiarism_update_status($this->get_course(), $this->get_course_module());
4588 if ($this->is_blind_marking() && has_capability('mod/assign:viewblinddetails', $this->get_context())) {
4589 $o .= $this->get_renderer()->notification(get_string('blindmarkingenabledwarning', 'assign'), 'notifymessage');
4592 // Load and print the table of submissions.
4593 if ($showquickgrading && $quickgrading) {
4594 $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, true);
4595 $table = $this->get_renderer()->render($gradingtable);
4596 $page = optional_param('page', null, PARAM_INT);
4597 $quickformparams = array('cm'=>$this->get_course_module()->id,
4598 'gradingtable'=>$table,
4599 'sendstudentnotifications' => $this->get_instance()->sendstudentnotifications,
4600 'page' => $page);
4601 $quickgradingform = new mod_assign_quick_grading_form(null, $quickformparams);
4603 $o .= $this->get_renderer()->render(new assign_form('quickgradingform', $quickgradingform));
4604 } else {
4605 $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, false);
4606 $o .= $this->get_renderer()->render($gradingtable);
4609 if ($this->can_grade()) {
4610 // We need to store the order of uses in the table as the person may wish to grade them.
4611 // This is done based on the row number of the user.
4612 $useridlist = $gradingtable->get_column_data('userid');
4613 $SESSION->mod_assign_useridlist[$this->get_useridlist_key()] = $useridlist;
4616 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4617 $users = array_keys($this->list_participants($currentgroup, true));
4618 if (count($users) != 0 && $this->can_grade()) {
4619 // If no enrolled user in a course then don't display the batch operations feature.
4620 $assignform = new assign_form('gradingbatchoperationsform', $gradingbatchoperationsform);
4621 $o .= $this->get_renderer()->render($assignform);
4623 $assignform = new assign_form('gradingoptionsform',
4624 $gradingoptionsform,
4625 'M.mod_assign.init_grading_options');
4626 $o .= $this->get_renderer()->render($assignform);
4627 return $o;
4631 * View entire grader app.
4633 * @return string
4635 protected function view_grader() {
4636 global $USER, $PAGE;
4638 $o = '';
4639 // Need submit permission to submit an assignment.
4640 $this->require_view_grades();
4642 $PAGE->set_pagelayout('embedded');
4644 $courseshortname = $this->get_context()->get_course_context()->get_context_name(false, true);
4645 $args = [
4646 'contextname' => $this->get_context()->get_context_name(false, true),
4647 'subpage' => get_string('grading', 'assign')
4649 $title = get_string('subpagetitle', 'assign', $args);
4650 $title = $courseshortname . ': ' . $title;
4651 $PAGE->set_title($title);
4653 $o .= $this->get_renderer()->header();
4655 $userid = optional_param('userid', 0, PARAM_INT);
4656 $blindid = optional_param('blindid', 0, PARAM_INT);
4658 if (!$userid && $blindid) {
4659 $userid = $this->get_user_id_for_uniqueid($blindid);
4662 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4663 $framegrader = new grading_app($userid, $currentgroup, $this);
4665 $this->update_effective_access($userid);
4667 $o .= $this->get_renderer()->render($framegrader);
4669 $o .= $this->view_footer();
4671 \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4673 return $o;
4676 * View entire grading page.
4678 * @return string
4680 protected function view_grading_page() {
4681 global $CFG;
4683 $o = '';
4684 // Need submit permission to submit an assignment.
4685 $this->require_view_grades();
4686 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
4688 $this->add_grade_notices();
4690 // Only load this if it is.
4691 $o .= $this->view_grading_table();
4693 $o .= $this->view_footer();
4695 \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4697 return $o;
4701 * Capture the output of the plagiarism plugins disclosures and return it as a string.
4703 * @return string
4705 protected function plagiarism_print_disclosure() {
4706 global $CFG;
4707 $o = '';
4709 if (!empty($CFG->enableplagiarism)) {
4710 require_once($CFG->libdir . '/plagiarismlib.php');
4712 $o .= plagiarism_print_disclosure($this->get_course_module()->id);
4715 return $o;
4719 * Message for students when assignment submissions have been closed.
4721 * @param string $title The page title
4722 * @param array $notices The array of notices to show.
4723 * @return string
4725 protected function view_notices($title, $notices) {
4726 global $CFG;
4728 $o = '';
4730 $header = new assign_header($this->get_instance(),
4731 $this->get_context(),
4732 $this->show_intro(),
4733 $this->get_course_module()->id,
4734 $title);
4735 $o .= $this->get_renderer()->render($header);
4737 foreach ($notices as $notice) {
4738 $o .= $this->get_renderer()->notification($notice);
4741 $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id, 'action'=>'view'));
4742 $o .= $this->get_renderer()->continue_button($url);
4744 $o .= $this->view_footer();
4746 return $o;
4750 * Get the name for a user - hiding their real name if blind marking is on.
4752 * @param stdClass $user The user record as required by fullname()
4753 * @return string The name.
4755 public function fullname($user) {
4756 if ($this->is_blind_marking()) {
4757 $hasviewblind = has_capability('mod/assign:viewblinddetails', $this->get_context());
4758 if (empty($user->recordid)) {
4759 $uniqueid = $this->get_uniqueid_for_user($user->id);
4760 } else {
4761 $uniqueid = $user->recordid;
4763 if ($hasviewblind) {
4764 return get_string('participant', 'assign') . ' ' . $uniqueid . ' (' .
4765 fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())) . ')';
4766 } else {
4767 return get_string('participant', 'assign') . ' ' . $uniqueid;
4769 } else {
4770 return fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context()));
4775 * View edit submissions page.
4777 * @param moodleform $mform
4778 * @param array $notices A list of notices to display at the top of the
4779 * edit submission form (e.g. from plugins).
4780 * @return string The page output.
4782 protected function view_edit_submission_page($mform, $notices) {
4783 global $CFG, $USER, $DB;
4785 $o = '';
4786 require_once($CFG->dirroot . '/mod/assign/submission_form.php');
4787 // Need submit permission to submit an assignment.
4788 $userid = optional_param('userid', $USER->id, PARAM_INT);
4789 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
4791 // This variation on the url will link direct to this student.
4792 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4793 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4794 $this->register_return_link('editsubmission', $returnparams);
4796 if ($userid == $USER->id) {
4797 if (!$this->can_edit_submission($userid, $USER->id)) {
4798 print_error('nopermission');
4800 // User is editing their own submission.
4801 require_capability('mod/assign:submit', $this->context);
4802 $title = get_string('editsubmission', 'assign');
4803 } else {
4804 // User is editing another user's submission.
4805 if (!$this->can_edit_submission($userid, $USER->id)) {
4806 print_error('nopermission');
4809 $name = $this->fullname($user);
4810 $title = get_string('editsubmissionother', 'assign', $name);
4813 if (!$this->submissions_open($userid)) {
4814 $message = array(get_string('submissionsclosed', 'assign'));
4815 return $this->view_notices($title, $message);
4818 $postfix = '';
4819 if ($this->has_visible_attachments()) {
4820 $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
4822 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
4823 $this->get_context(),
4824 $this->show_intro(),
4825 $this->get_course_module()->id,
4826 $title, '', $postfix));
4828 // Show plagiarism disclosure for any user submitter.
4829 $o .= $this->plagiarism_print_disclosure();
4831 $data = new stdClass();
4832 $data->userid = $userid;
4833 if (!$mform) {
4834 $mform = new mod_assign_submission_form(null, array($this, $data));
4837 foreach ($notices as $notice) {
4838 $o .= $this->get_renderer()->notification($notice);
4841 $o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
4843 $o .= $this->view_footer();
4845 \mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger();
4847 return $o;
4851 * See if this assignment has a grade yet.
4853 * @param int $userid
4854 * @return bool
4856 protected function is_graded($userid) {
4857 $grade = $this->get_user_grade($userid, false);
4858 if ($grade) {
4859 return ($grade->grade !== null && $grade->grade >= 0);
4861 return false;
4865 * Perform an access check to see if the current $USER can edit this group submission.
4867 * @param int $groupid
4868 * @return bool
4870 public function can_edit_group_submission($groupid) {
4871 global $USER;
4873 $members = $this->get_submission_group_members($groupid, true);
4874 foreach ($members as $member) {
4875 // If we can edit any members submission, we can edit the submission for the group.
4876 if ($this->can_edit_submission($member->id)) {
4877 return true;
4880 return false;
4884 * Perform an access check to see if the current $USER can view this group submission.
4886 * @param int $groupid
4887 * @return bool
4889 public function can_view_group_submission($groupid) {
4890 global $USER;
4892 $members = $this->get_submission_group_members($groupid, true);
4893 foreach ($members as $member) {
4894 // If we can view any members submission, we can view the submission for the group.
4895 if ($this->can_view_submission($member->id)) {
4896 return true;
4899 return false;
4903 * Perform an access check to see if the current $USER can view this users submission.
4905 * @param int $userid
4906 * @return bool
4908 public function can_view_submission($userid) {
4909 global $USER;
4911 if (!$this->is_active_user($userid) && !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
4912 return false;
4914 if (!is_enrolled($this->get_course_context(), $userid)) {
4915 return false;
4917 if (has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
4918 return true;
4920 if ($userid == $USER->id) {
4921 return true;
4923 return false;
4927 * Allows the plugin to show a batch grading operation page.
4929 * @param moodleform $mform
4930 * @return none
4932 protected function view_plugin_grading_batch_operation($mform) {
4933 require_capability('mod/assign:grade', $this->context);
4934 $prefix = 'plugingradingbatchoperation_';
4936 if ($data = $mform->get_data()) {
4937 $tail = substr($data->operation, strlen($prefix));
4938 list($plugintype, $action) = explode('_', $tail, 2);
4940 $plugin = $this->get_feedback_plugin_by_type($plugintype);
4941 if ($plugin) {
4942 $users = $data->selectedusers;
4943 $userlist = explode(',', $users);
4944 echo $plugin->grading_batch_operation($action, $userlist);
4945 return;
4948 print_error('invalidformdata', '');
4952 * Ask the user to confirm they want to perform this batch operation
4954 * @param moodleform $mform Set to a grading batch operations form
4955 * @return string - the page to view after processing these actions
4957 protected function process_grading_batch_operation(& $mform) {
4958 global $CFG;
4959 require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
4960 require_sesskey();
4962 $markingallocation = $this->get_instance()->markingworkflow &&
4963 $this->get_instance()->markingallocation &&
4964 has_capability('mod/assign:manageallocations', $this->context);
4966 $batchformparams = array('cm'=>$this->get_course_module()->id,
4967 'submissiondrafts'=>$this->get_instance()->submissiondrafts,
4968 'duedate'=>$this->get_instance()->duedate,
4969 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
4970 'feedbackplugins'=>$this->get_feedback_plugins(),
4971 'context'=>$this->get_context(),
4972 'markingworkflow'=>$this->get_instance()->markingworkflow,
4973 'markingallocation'=>$markingallocation);
4974 $formclasses = array('class'=>'gradingbatchoperationsform');
4975 $mform = new mod_assign_grading_batch_operations_form(null,
4976 $batchformparams,
4977 'post',
4979 $formclasses);
4981 if ($data = $mform->get_data()) {
4982 // Get the list of users.
4983 $users = $data->selectedusers;
4984 $userlist = explode(',', $users);
4986 $prefix = 'plugingradingbatchoperation_';
4988 if ($data->operation == 'grantextension') {
4989 // Reset the form so the grant extension page will create the extension form.
4990 $mform = null;
4991 return 'grantextension';
4992 } else if ($data->operation == 'setmarkingworkflowstate') {
4993 return 'viewbatchsetmarkingworkflowstate';
4994 } else if ($data->operation == 'setmarkingallocation') {
4995 return 'viewbatchmarkingallocation';
4996 } else if (strpos($data->operation, $prefix) === 0) {
4997 $tail = substr($data->operation, strlen($prefix));
4998 list($plugintype, $action) = explode('_', $tail, 2);
5000 $plugin = $this->get_feedback_plugin_by_type($plugintype);
5001 if ($plugin) {
5002 return 'plugingradingbatchoperation';
5006 if ($data->operation == 'downloadselected') {
5007 $this->download_submissions($userlist);
5008 } else {
5009 foreach ($userlist as $userid) {
5010 if ($data->operation == 'lock') {
5011 $this->process_lock_submission($userid);
5012 } else if ($data->operation == 'unlock') {
5013 $this->process_unlock_submission($userid);
5014 } else if ($data->operation == 'reverttodraft') {
5015 $this->process_revert_to_draft($userid);
5016 } else if ($data->operation == 'removesubmission') {
5017 $this->process_remove_submission($userid);
5018 } else if ($data->operation == 'addattempt') {
5019 if (!$this->get_instance()->teamsubmission) {
5020 $this->process_add_attempt($userid);
5025 if ($this->get_instance()->teamsubmission && $data->operation == 'addattempt') {
5026 // This needs to be handled separately so that each team submission is only re-opened one time.
5027 $this->process_add_attempt_group($userlist);
5031 return 'grading';
5035 * Shows a form that allows the workflow state for selected submissions to be changed.
5037 * @param moodleform $mform Set to a grading batch operations form
5038 * @return string - the page to view after processing these actions
5040 protected function view_batch_set_workflow_state($mform) {
5041 global $CFG, $DB;
5043 require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
5045 $o = '';
5047 $submitteddata = $mform->get_data();
5048 $users = $submitteddata->selectedusers;
5049 $userlist = explode(',', $users);
5051 $formdata = array('id' => $this->get_course_module()->id,
5052 'selectedusers' => $users);
5054 $usershtml = '';
5056 $usercount = 0;
5057 // TODO Does not support custom user profile fields (MDL-70456).
5058 $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false);
5059 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5060 foreach ($userlist as $userid) {
5061 if ($usercount >= 5) {
5062 $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
5063 break;
5065 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
5067 $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
5068 $this->get_course()->id,
5069 $viewfullnames,
5070 $this->is_blind_marking(),
5071 $this->get_uniqueid_for_user($user->id),
5072 $extrauserfields,
5073 !$this->is_active_user($userid)));
5074 $usercount += 1;
5077 $formparams = array(
5078 'userscount' => count($userlist),
5079 'usershtml' => $usershtml,
5080 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
5083 $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
5084 $mform->set_data($formdata); // Initialises the hidden elements.
5085 $header = new assign_header($this->get_instance(),
5086 $this->get_context(),
5087 $this->show_intro(),
5088 $this->get_course_module()->id,
5089 get_string('setmarkingworkflowstate', 'assign'));
5090 $o .= $this->get_renderer()->render($header);
5091 $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
5092 $o .= $this->view_footer();
5094 \mod_assign\event\batch_set_workflow_state_viewed::create_from_assign($this)->trigger();
5096 return $o;
5100 * Shows a form that allows the allocated marker for selected submissions to be changed.
5102 * @param moodleform $mform Set to a grading batch operations form
5103 * @return string - the page to view after processing these actions
5105 public function view_batch_markingallocation($mform) {
5106 global $CFG, $DB;
5108 require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
5110 $o = '';
5112 $submitteddata = $mform->get_data();
5113 $users = $submitteddata->selectedusers;
5114 $userlist = explode(',', $users);
5116 $formdata = array('id' => $this->get_course_module()->id,
5117 'selectedusers' => $users);
5119 $usershtml = '';
5121 $usercount = 0;
5122 // TODO Does not support custom user profile fields (MDL-70456).
5123 $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false);
5124 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5125 foreach ($userlist as $userid) {
5126 if ($usercount >= 5) {
5127 $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
5128 break;
5130 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
5132 $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
5133 $this->get_course()->id,
5134 $viewfullnames,
5135 $this->is_blind_marking(),
5136 $this->get_uniqueid_for_user($user->id),
5137 $extrauserfields,
5138 !$this->is_active_user($userid)));
5139 $usercount += 1;
5142 $formparams = array(
5143 'userscount' => count($userlist),
5144 'usershtml' => $usershtml,
5147 list($sort, $params) = users_order_by_sql('u');
5148 // Only enrolled users could be assigned as potential markers.
5149 $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
5150 $markerlist = array();
5151 foreach ($markers as $marker) {
5152 $markerlist[$marker->id] = fullname($marker);
5155 $formparams['markers'] = $markerlist;
5157 $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
5158 $mform->set_data($formdata); // Initialises the hidden elements.
5159 $header = new assign_header($this->get_instance(),
5160 $this->get_context(),
5161 $this->show_intro(),
5162 $this->get_course_module()->id,
5163 get_string('setmarkingallocation', 'assign'));
5164 $o .= $this->get_renderer()->render($header);
5165 $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
5166 $o .= $this->view_footer();
5168 \mod_assign\event\batch_set_marker_allocation_viewed::create_from_assign($this)->trigger();
5170 return $o;
5174 * Ask the user to confirm they want to submit their work for grading.
5176 * @param moodleform $mform - null unless form validation has failed
5177 * @return string
5179 protected function check_submit_for_grading($mform) {
5180 global $USER, $CFG;
5182 require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
5184 // Check that all of the submission plugins are ready for this submission.
5185 // Also check whether there is something to be submitted as well against atleast one.
5186 $notifications = array();
5187 $submission = $this->get_user_submission($USER->id, false);
5188 if ($this->get_instance()->teamsubmission) {
5189 $submission = $this->get_group_submission($USER->id, 0, false);
5192 $plugins = $this->get_submission_plugins();
5193 $hassubmission = false;
5194 foreach ($plugins as $plugin) {
5195 if ($plugin->is_enabled() && $plugin->is_visible()) {
5196 $check = $plugin->precheck_submission($submission);
5197 if ($check !== true) {
5198 $notifications[] = $check;
5201 if (is_object($submission) && !$plugin->is_empty($submission)) {
5202 $hassubmission = true;
5207 // If there are no submissions and no existing notifications to be displayed the stop.
5208 if (!$hassubmission && !$notifications) {
5209 $notifications[] = get_string('addsubmission_help', 'assign');
5212 $data = new stdClass();
5213 $adminconfig = $this->get_admin_config();
5214 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
5215 $submissionstatement = '';
5217 if ($requiresubmissionstatement) {
5218 $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
5221 // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
5222 // that the submission statement checkbox will be displayed.
5223 if (empty($submissionstatement)) {
5224 $requiresubmissionstatement = false;
5227 if ($mform == null) {
5228 $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
5229 $submissionstatement,
5230 $this->get_course_module()->id,
5231 $data));
5233 $o = '';
5234 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
5235 $this->get_context(),
5236 $this->show_intro(),
5237 $this->get_course_module()->id,
5238 get_string('confirmsubmissionheading', 'assign')));
5239 $submitforgradingpage = new assign_submit_for_grading_page($notifications,
5240 $this->get_course_module()->id,
5241 $mform);
5242 $o .= $this->get_renderer()->render($submitforgradingpage);
5243 $o .= $this->view_footer();
5245 \mod_assign\event\submission_confirmation_form_viewed::create_from_assign($this)->trigger();
5247 return $o;
5251 * Creates an assign_submission_status renderable.
5253 * @param stdClass $user the user to get the report for
5254 * @param bool $showlinks return plain text or links to the profile
5255 * @return assign_submission_status renderable object
5257 public function get_assign_submission_status_renderable($user, $showlinks) {
5258 global $PAGE;
5260 $instance = $this->get_instance();
5261 $flags = $this->get_user_flags($user->id, false);
5262 $submission = $this->get_user_submission($user->id, false);
5264 $teamsubmission = null;
5265 $submissiongroup = null;
5266 $notsubmitted = array();
5267 if ($instance->teamsubmission) {
5268 $teamsubmission = $this->get_group_submission($user->id, 0, false);
5269 $submissiongroup = $this->get_submission_group($user->id);
5270 $groupid = 0;
5271 if ($submissiongroup) {
5272 $groupid = $submissiongroup->id;
5274 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
5277 $showedit = $showlinks &&
5278 ($this->is_any_submission_plugin_enabled()) &&
5279 $this->can_edit_submission($user->id);
5281 $submissionlocked = ($flags && $flags->locked);
5283 // Grading criteria preview.
5284 $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
5285 $gradingcontrollerpreview = '';
5286 if ($gradingmethod = $gradingmanager->get_active_method()) {
5287 $controller = $gradingmanager->get_controller($gradingmethod);
5288 if ($controller->is_form_defined()) {
5289 $gradingcontrollerpreview = $controller->render_preview($PAGE);
5293 $showsubmit = ($showlinks && $this->submissions_open($user->id));
5294 $showsubmit = ($showsubmit && $this->show_submit_button($submission, $teamsubmission, $user->id));
5296 $extensionduedate = null;
5297 if ($flags) {
5298 $extensionduedate = $flags->extensionduedate;
5300 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5302 $gradingstatus = $this->get_grading_status($user->id);
5303 $usergroups = $this->get_all_groups($user->id);
5304 $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
5305 $instance->alwaysshowdescription,
5306 $submission,
5307 $instance->teamsubmission,
5308 $teamsubmission,
5309 $submissiongroup,
5310 $notsubmitted,
5311 $this->is_any_submission_plugin_enabled(),
5312 $submissionlocked,
5313 $this->is_graded($user->id),
5314 $instance->duedate,
5315 $instance->cutoffdate,
5316 $this->get_submission_plugins(),
5317 $this->get_return_action(),
5318 $this->get_return_params(),
5319 $this->get_course_module()->id,
5320 $this->get_course()->id,
5321 assign_submission_status::STUDENT_VIEW,
5322 $showedit,
5323 $showsubmit,
5324 $viewfullnames,
5325 $extensionduedate,
5326 $this->get_context(),
5327 $this->is_blind_marking(),
5328 $gradingcontrollerpreview,
5329 $instance->attemptreopenmethod,
5330 $instance->maxattempts,
5331 $gradingstatus,
5332 $instance->preventsubmissionnotingroup,
5333 $usergroups);
5334 return $submissionstatus;
5339 * Creates an assign_feedback_status renderable.
5341 * @param stdClass $user the user to get the report for
5342 * @return assign_feedback_status renderable object
5344 public function get_assign_feedback_status_renderable($user) {
5345 global $CFG, $DB, $PAGE;
5347 require_once($CFG->libdir.'/gradelib.php');
5348 require_once($CFG->dirroot.'/grade/grading/lib.php');
5350 $instance = $this->get_instance();
5351 $grade = $this->get_user_grade($user->id, false);
5352 $gradingstatus = $this->get_grading_status($user->id);
5354 $gradinginfo = grade_get_grades($this->get_course()->id,
5355 'mod',
5356 'assign',
5357 $instance->id,
5358 $user->id);
5360 $gradingitem = null;
5361 $gradebookgrade = null;
5362 if (isset($gradinginfo->items[0])) {
5363 $gradingitem = $gradinginfo->items[0];
5364 $gradebookgrade = $gradingitem->grades[$user->id];
5367 // Check to see if all feedback plugins are empty.
5368 $emptyplugins = true;
5369 if ($grade) {
5370 foreach ($this->get_feedback_plugins() as $plugin) {
5371 if ($plugin->is_visible() && $plugin->is_enabled()) {
5372 if (!$plugin->is_empty($grade)) {
5373 $emptyplugins = false;
5379 if ($this->get_instance()->markingworkflow && $gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
5380 $emptyplugins = true; // Don't show feedback plugins until released either.
5383 $cangrade = has_capability('mod/assign:grade', $this->get_context());
5384 $hasgrade = $this->get_instance()->grade != GRADE_TYPE_NONE &&
5385 !is_null($gradebookgrade) && !is_null($gradebookgrade->grade);
5386 $gradevisible = $cangrade || $this->get_instance()->grade == GRADE_TYPE_NONE ||
5387 (!is_null($gradebookgrade) && !$gradebookgrade->hidden);
5388 // If there is a visible grade, show the summary.
5389 if (($hasgrade || !$emptyplugins) && $gradevisible) {
5391 $gradefordisplay = null;
5392 $gradeddate = null;
5393 $grader = null;
5394 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5396 if ($hasgrade) {
5397 if ($controller = $gradingmanager->get_active_controller()) {
5398 $menu = make_grades_menu($this->get_instance()->grade);
5399 $controller->set_grade_range($menu, $this->get_instance()->grade > 0);
5400 $gradefordisplay = $controller->render_grade($PAGE,
5401 $grade->id,
5402 $gradingitem,
5403 $gradebookgrade->str_long_grade,
5404 $cangrade);
5405 } else {
5406 $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
5408 $gradeddate = $gradebookgrade->dategraded;
5410 // Only display the grader if it is in the right state.
5411 if (in_array($gradingstatus, [ASSIGN_GRADING_STATUS_GRADED, ASSIGN_MARKING_WORKFLOW_STATE_RELEASED])){
5412 if (isset($grade->grader) && $grade->grader > 0) {
5413 $grader = $DB->get_record('user', array('id' => $grade->grader));
5414 } else if (isset($gradebookgrade->usermodified)
5415 && $gradebookgrade->usermodified > 0
5416 && has_capability('mod/assign:grade', $this->get_context(), $gradebookgrade->usermodified)) {
5417 // Grader not provided. Check that usermodified is a user who can grade.
5418 // Case 1: When an assignment is reopened an empty assign_grade is created so the feedback
5419 // plugin can know which attempt it's referring to. In this case, usermodifed is a student.
5420 // Case 2: When an assignment's grade is overrided via the gradebook, usermodified is a grader
5421 $grader = $DB->get_record('user', array('id' => $gradebookgrade->usermodified));
5426 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5428 if ($grade) {
5429 \mod_assign\event\feedback_viewed::create_from_grade($this, $grade)->trigger();
5431 $feedbackstatus = new assign_feedback_status($gradefordisplay,
5432 $gradeddate,
5433 $grader,
5434 $this->get_feedback_plugins(),
5435 $grade,
5436 $this->get_course_module()->id,
5437 $this->get_return_action(),
5438 $this->get_return_params(),
5439 $viewfullnames);
5441 // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability.
5442 $showgradername = (
5443 has_capability('mod/assign:showhiddengrader', $this->context) or
5444 !$this->is_hidden_grader()
5447 if (!$showgradername) {
5448 $feedbackstatus->grader = false;
5451 return $feedbackstatus;
5453 return;
5457 * Creates an assign_attempt_history renderable.
5459 * @param stdClass $user the user to get the report for
5460 * @return assign_attempt_history renderable object
5462 public function get_assign_attempt_history_renderable($user) {
5464 $allsubmissions = $this->get_all_submissions($user->id);
5465 $allgrades = $this->get_all_grades($user->id);
5467 $history = new assign_attempt_history($allsubmissions,
5468 $allgrades,
5469 $this->get_submission_plugins(),
5470 $this->get_feedback_plugins(),
5471 $this->get_course_module()->id,
5472 $this->get_return_action(),
5473 $this->get_return_params(),
5474 false,
5477 return $history;
5481 * Print 2 tables of information with no action links -
5482 * the submission summary and the grading summary.
5484 * @param stdClass $user the user to print the report for
5485 * @param bool $showlinks - Return plain text or links to the profile
5486 * @return string - the html summary
5488 public function view_student_summary($user, $showlinks) {
5490 $o = '';
5492 if ($this->can_view_submission($user->id)) {
5493 if (has_capability('mod/assign:viewownsubmissionsummary', $this->get_context(), $user, false)) {
5494 // The user can view the submission summary.
5495 $submissionstatus = $this->get_assign_submission_status_renderable($user, $showlinks);
5496 $o .= $this->get_renderer()->render($submissionstatus);
5499 // If there is a visible grade, show the feedback.
5500 $feedbackstatus = $this->get_assign_feedback_status_renderable($user);
5501 if ($feedbackstatus) {
5502 $o .= $this->get_renderer()->render($feedbackstatus);
5505 // If there is more than one submission, show the history.
5506 $history = $this->get_assign_attempt_history_renderable($user);
5507 if (count($history->submissions) > 1) {
5508 $o .= $this->get_renderer()->render($history);
5511 return $o;
5515 * Returns true if the submit subsission button should be shown to the user.
5517 * @param stdClass $submission The users own submission record.
5518 * @param stdClass $teamsubmission The users team submission record if there is one
5519 * @param int $userid The user
5520 * @return bool
5522 protected function show_submit_button($submission = null, $teamsubmission = null, $userid = null) {
5523 if (!has_capability('mod/assign:submit', $this->get_context(), $userid, false)) {
5524 // The user does not have the capability to submit.
5525 return false;
5527 if ($teamsubmission) {
5528 if ($teamsubmission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5529 // The assignment submission has been completed.
5530 return false;
5531 } else if ($this->submission_empty($teamsubmission)) {
5532 // There is nothing to submit yet.
5533 return false;
5534 } else if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5535 // The user has already clicked the submit button on the team submission.
5536 return false;
5537 } else if (
5538 !empty($this->get_instance()->preventsubmissionnotingroup)
5539 && $this->get_submission_group($userid) == false
5541 return false;
5543 } else if ($submission) {
5544 if ($submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5545 // The assignment submission has been completed.
5546 return false;
5547 } else if ($this->submission_empty($submission)) {
5548 // There is nothing to submit.
5549 return false;
5551 } else {
5552 // We've not got a valid submission or team submission.
5553 return false;
5555 // Last check is that this instance allows drafts.
5556 return $this->get_instance()->submissiondrafts;
5560 * Get the grades for all previous attempts.
5561 * For each grade - the grader is a full user record,
5562 * and gradefordisplay is added (rendered from grading manager).
5564 * @param int $userid If not set, $USER->id will be used.
5565 * @return array $grades All grade records for this user.
5567 protected function get_all_grades($userid) {
5568 global $DB, $USER, $PAGE;
5570 // If the userid is not null then use userid.
5571 if (!$userid) {
5572 $userid = $USER->id;
5575 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
5577 $grades = $DB->get_records('assign_grades', $params, 'attemptnumber ASC');
5579 $gradercache = array();
5580 $cangrade = has_capability('mod/assign:grade', $this->get_context());
5582 // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability.
5583 $showgradername = (
5584 has_capability('mod/assign:showhiddengrader', $this->context, $userid) or
5585 !$this->is_hidden_grader()
5588 // Need gradingitem and gradingmanager.
5589 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5590 $controller = $gradingmanager->get_active_controller();
5592 $gradinginfo = grade_get_grades($this->get_course()->id,
5593 'mod',
5594 'assign',
5595 $this->get_instance()->id,
5596 $userid);
5598 $gradingitem = null;
5599 if (isset($gradinginfo->items[0])) {
5600 $gradingitem = $gradinginfo->items[0];
5603 foreach ($grades as $grade) {
5604 // First lookup the grader info.
5605 if (!$showgradername) {
5606 $grade->grader = null;
5607 } else if (isset($gradercache[$grade->grader])) {
5608 $grade->grader = $gradercache[$grade->grader];
5609 } else if ($grade->grader > 0) {
5610 // Not in cache - need to load the grader record.
5611 $grade->grader = $DB->get_record('user', array('id'=>$grade->grader));
5612 if ($grade->grader) {
5613 $gradercache[$grade->grader->id] = $grade->grader;
5617 // Now get the gradefordisplay.
5618 if ($controller) {
5619 $controller->set_grade_range(make_grades_menu($this->get_instance()->grade), $this->get_instance()->grade > 0);
5620 $grade->gradefordisplay = $controller->render_grade($PAGE,
5621 $grade->id,
5622 $gradingitem,
5623 $grade->grade,
5624 $cangrade);
5625 } else {
5626 $grade->gradefordisplay = $this->display_grade($grade->grade, false);
5631 return $grades;
5635 * Get the submissions for all previous attempts.
5637 * @param int $userid If not set, $USER->id will be used.
5638 * @return array $submissions All submission records for this user (or group).
5640 public function get_all_submissions($userid) {
5641 global $DB, $USER;
5643 // If the userid is not null then use userid.
5644 if (!$userid) {
5645 $userid = $USER->id;
5648 $params = array();
5650 if ($this->get_instance()->teamsubmission) {
5651 $groupid = 0;
5652 $group = $this->get_submission_group($userid);
5653 if ($group) {
5654 $groupid = $group->id;
5657 // Params to get the group submissions.
5658 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
5659 } else {
5660 // Params to get the user submissions.
5661 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
5664 // Return the submissions ordered by attempt.
5665 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber ASC');
5667 return $submissions;
5671 * Creates an assign_grading_summary renderable.
5673 * @param mixed $activitygroup int|null the group for calculating the grading summary (if null the function will determine it)
5674 * @return assign_grading_summary renderable object
5676 public function get_assign_grading_summary_renderable($activitygroup = null) {
5678 $instance = $this->get_default_instance(); // Grading summary requires the raw dates, regardless of relativedates mode.
5679 $cm = $this->get_course_module();
5680 $course = $this->get_course();
5682 $draft = ASSIGN_SUBMISSION_STATUS_DRAFT;
5683 $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5684 $isvisible = $cm->visible;
5686 if ($activitygroup === null) {
5687 $activitygroup = groups_get_activity_group($cm);
5690 if ($instance->teamsubmission) {
5691 $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_NO;
5692 $defaultteammembers = $this->get_submission_group_members(0, true);
5693 if (count($defaultteammembers) > 0) {
5694 if ($instance->preventsubmissionnotingroup) {
5695 $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_REQUIRED;
5696 } else {
5697 $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_OPTIONAL;
5701 $summary = new assign_grading_summary(
5702 $this->count_teams($activitygroup),
5703 $instance->submissiondrafts,
5704 $this->count_submissions_with_status($draft, $activitygroup),
5705 $this->is_any_submission_plugin_enabled(),
5706 $this->count_submissions_with_status($submitted, $activitygroup),
5707 $instance->cutoffdate,
5708 $this->get_duedate($activitygroup),
5709 $this->get_course_module()->id,
5710 $this->count_submissions_need_grading($activitygroup),
5711 $instance->teamsubmission,
5712 $warnofungroupedusers,
5713 $course->relativedatesmode,
5714 $course->startdate,
5715 $this->can_grade(),
5716 $isvisible
5718 } else {
5719 // The active group has already been updated in groups_print_activity_menu().
5720 $countparticipants = $this->count_participants($activitygroup);
5721 $summary = new assign_grading_summary(
5722 $countparticipants,
5723 $instance->submissiondrafts,
5724 $this->count_submissions_with_status($draft, $activitygroup),
5725 $this->is_any_submission_plugin_enabled(),
5726 $this->count_submissions_with_status($submitted, $activitygroup),
5727 $instance->cutoffdate,
5728 $this->get_duedate($activitygroup),
5729 $this->get_course_module()->id,
5730 $this->count_submissions_need_grading($activitygroup),
5731 $instance->teamsubmission,
5732 assign_grading_summary::WARN_GROUPS_NO,
5733 $course->relativedatesmode,
5734 $course->startdate,
5735 $this->can_grade(),
5736 $isvisible
5740 return $summary;
5744 * Return group override duedate.
5746 * @param int $activitygroup Activity active group
5747 * @return int $duedate
5749 private function get_duedate($activitygroup = null) {
5750 global $DB;
5752 if ($activitygroup === null) {
5753 $activitygroup = groups_get_activity_group($this->get_course_module());
5755 if ($this->can_view_grades()) {
5756 $params = array('groupid' => $activitygroup, 'assignid' => $this->get_instance()->id);
5757 $groupoverride = $DB->get_record('assign_overrides', $params);
5758 if (!empty($groupoverride->duedate)) {
5759 return $groupoverride->duedate;
5762 return $this->get_instance()->duedate;
5766 * View submissions page (contains details of current submission).
5768 * @return string
5770 protected function view_submission_page() {
5771 global $CFG, $DB, $USER, $PAGE;
5773 $instance = $this->get_instance();
5775 $this->add_grade_notices();
5777 $o = '';
5779 $postfix = '';
5780 if ($this->has_visible_attachments()) {
5781 $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
5783 $o .= $this->get_renderer()->render(new assign_header($instance,
5784 $this->get_context(),
5785 $this->show_intro(),
5786 $this->get_course_module()->id,
5787 '', '', $postfix));
5789 // Display plugin specific headers.
5790 $plugins = array_merge($this->get_submission_plugins(), $this->get_feedback_plugins());
5791 foreach ($plugins as $plugin) {
5792 if ($plugin->is_enabled() && $plugin->is_visible()) {
5793 $o .= $this->get_renderer()->render(new assign_plugin_header($plugin));
5797 if ($this->can_view_grades()) {
5798 // Group selector will only be displayed if necessary.
5799 $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
5800 $o .= groups_print_activity_menu($this->get_course_module(), $currenturl->out(), true);
5802 $summary = $this->get_assign_grading_summary_renderable();
5803 $o .= $this->get_renderer()->render($summary);
5805 $grade = $this->get_user_grade($USER->id, false);
5806 $submission = $this->get_user_submission($USER->id, false);
5808 if ($this->can_view_submission($USER->id)) {
5809 $o .= $this->view_student_summary($USER, true);
5812 $o .= $this->view_footer();
5814 \mod_assign\event\submission_status_viewed::create_from_assign($this)->trigger();
5816 return $o;
5820 * Convert the final raw grade(s) in the grading table for the gradebook.
5822 * @param stdClass $grade
5823 * @return array
5825 protected function convert_grade_for_gradebook(stdClass $grade) {
5826 $gradebookgrade = array();
5827 if ($grade->grade >= 0) {
5828 $gradebookgrade['rawgrade'] = $grade->grade;
5830 // Allow "no grade" to be chosen.
5831 if ($grade->grade == -1) {
5832 $gradebookgrade['rawgrade'] = NULL;
5834 $gradebookgrade['userid'] = $grade->userid;
5835 $gradebookgrade['usermodified'] = $grade->grader;
5836 $gradebookgrade['datesubmitted'] = null;
5837 $gradebookgrade['dategraded'] = $grade->timemodified;
5838 if (isset($grade->feedbackformat)) {
5839 $gradebookgrade['feedbackformat'] = $grade->feedbackformat;
5841 if (isset($grade->feedbacktext)) {
5842 $gradebookgrade['feedback'] = $grade->feedbacktext;
5844 if (isset($grade->feedbackfiles)) {
5845 $gradebookgrade['feedbackfiles'] = $grade->feedbackfiles;
5848 return $gradebookgrade;
5852 * Convert submission details for the gradebook.
5854 * @param stdClass $submission
5855 * @return array
5857 protected function convert_submission_for_gradebook(stdClass $submission) {
5858 $gradebookgrade = array();
5860 $gradebookgrade['userid'] = $submission->userid;
5861 $gradebookgrade['usermodified'] = $submission->userid;
5862 $gradebookgrade['datesubmitted'] = $submission->timemodified;
5864 return $gradebookgrade;
5868 * Update grades in the gradebook.
5870 * @param mixed $submission stdClass|null
5871 * @param mixed $grade stdClass|null
5872 * @return bool
5874 protected function gradebook_item_update($submission=null, $grade=null) {
5875 global $CFG;
5877 require_once($CFG->dirroot.'/mod/assign/lib.php');
5878 // Do not push grade to gradebook if blind marking is active as
5879 // the gradebook would reveal the students.
5880 if ($this->is_blind_marking()) {
5881 return false;
5884 // If marking workflow is enabled and grade is not released then remove any grade that may exist in the gradebook.
5885 if ($this->get_instance()->markingworkflow && !empty($grade) &&
5886 $this->get_grading_status($grade->userid) != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
5887 // Remove the grade (if it exists) from the gradebook as it is not 'final'.
5888 $grade->grade = -1;
5889 $grade->feedbacktext = '';
5890 $grade->feebackfiles = [];
5893 if ($submission != null) {
5894 if ($submission->userid == 0) {
5895 // This is a group submission update.
5896 $team = groups_get_members($submission->groupid, 'u.id');
5898 foreach ($team as $member) {
5899 $membersubmission = clone $submission;
5900 $membersubmission->groupid = 0;
5901 $membersubmission->userid = $member->id;
5902 $this->gradebook_item_update($membersubmission, null);
5904 return;
5907 $gradebookgrade = $this->convert_submission_for_gradebook($submission);
5909 } else {
5910 $gradebookgrade = $this->convert_grade_for_gradebook($grade);
5912 // Grading is disabled, return.
5913 if ($this->grading_disabled($gradebookgrade['userid'])) {
5914 return false;
5916 $assign = clone $this->get_instance();
5917 $assign->cmidnumber = $this->get_course_module()->idnumber;
5918 // Set assign gradebook feedback plugin status (enabled and visible).
5919 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
5920 return assign_grade_item_update($assign, $gradebookgrade) == GRADE_UPDATE_OK;
5924 * Update team submission.
5926 * @param stdClass $submission
5927 * @param int $userid
5928 * @param bool $updatetime
5929 * @return bool
5931 protected function update_team_submission(stdClass $submission, $userid, $updatetime) {
5932 global $DB;
5934 if ($updatetime) {
5935 $submission->timemodified = time();
5938 // First update the submission for the current user.
5939 $mysubmission = $this->get_user_submission($userid, true, $submission->attemptnumber);
5940 $mysubmission->status = $submission->status;
5942 $this->update_submission($mysubmission, 0, $updatetime, false);
5944 // Now check the team settings to see if this assignment qualifies as submitted or draft.
5945 $team = $this->get_submission_group_members($submission->groupid, true);
5947 $allsubmitted = true;
5948 $anysubmitted = false;
5949 $result = true;
5950 if (!in_array($submission->status, [ASSIGN_SUBMISSION_STATUS_NEW, ASSIGN_SUBMISSION_STATUS_REOPENED])) {
5951 foreach ($team as $member) {
5952 $membersubmission = $this->get_user_submission($member->id, false, $submission->attemptnumber);
5954 // If no submission found for team member and member is active then everyone has not submitted.
5955 if (!$membersubmission || $membersubmission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED
5956 && ($this->is_active_user($member->id))) {
5957 $allsubmitted = false;
5958 if ($anysubmitted) {
5959 break;
5961 } else {
5962 $anysubmitted = true;
5965 if ($this->get_instance()->requireallteammemberssubmit) {
5966 if ($allsubmitted) {
5967 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5968 } else {
5969 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
5971 $result = $DB->update_record('assign_submission', $submission);
5972 } else {
5973 if ($anysubmitted) {
5974 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5975 } else {
5976 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
5978 $result = $DB->update_record('assign_submission', $submission);
5980 } else {
5981 // Set the group submission to reopened.
5982 foreach ($team as $member) {
5983 $membersubmission = $this->get_user_submission($member->id, true, $submission->attemptnumber);
5984 $membersubmission->status = $submission->status;
5985 $result = $DB->update_record('assign_submission', $membersubmission) && $result;
5987 $result = $DB->update_record('assign_submission', $submission) && $result;
5990 $this->gradebook_item_update($submission);
5991 return $result;
5995 * Update grades in the gradebook based on submission time.
5997 * @param stdClass $submission
5998 * @param int $userid
5999 * @param bool $updatetime
6000 * @param bool $teamsubmission
6001 * @return bool
6003 protected function update_submission(stdClass $submission, $userid, $updatetime, $teamsubmission) {
6004 global $DB;
6006 if ($teamsubmission) {
6007 return $this->update_team_submission($submission, $userid, $updatetime);
6010 if ($updatetime) {
6011 $submission->timemodified = time();
6013 $result= $DB->update_record('assign_submission', $submission);
6014 if ($result) {
6015 $this->gradebook_item_update($submission);
6017 return $result;
6021 * Is this assignment open for submissions?
6023 * Check the due date,
6024 * prevent late submissions,
6025 * has this person already submitted,
6026 * is the assignment locked?
6028 * @param int $userid - Optional userid so we can see if a different user can submit
6029 * @param bool $skipenrolled - Skip enrollment checks (because they have been done already)
6030 * @param stdClass $submission - Pre-fetched submission record (or false to fetch it)
6031 * @param stdClass $flags - Pre-fetched user flags record (or false to fetch it)
6032 * @param stdClass $gradinginfo - Pre-fetched user gradinginfo record (or false to fetch it)
6033 * @return bool
6035 public function submissions_open($userid = 0,
6036 $skipenrolled = false,
6037 $submission = false,
6038 $flags = false,
6039 $gradinginfo = false) {
6040 global $USER;
6042 if (!$userid) {
6043 $userid = $USER->id;
6046 $time = time();
6047 $dateopen = true;
6048 $finaldate = false;
6049 if ($this->get_instance()->cutoffdate) {
6050 $finaldate = $this->get_instance()->cutoffdate;
6053 if ($flags === false) {
6054 $flags = $this->get_user_flags($userid, false);
6056 if ($flags && $flags->locked) {
6057 return false;
6060 // User extensions.
6061 if ($finaldate) {
6062 if ($flags && $flags->extensionduedate) {
6063 // Extension can be before cut off date.
6064 if ($flags->extensionduedate > $finaldate) {
6065 $finaldate = $flags->extensionduedate;
6070 if ($finaldate) {
6071 $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time && $time <= $finaldate);
6072 } else {
6073 $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time);
6076 if (!$dateopen) {
6077 return false;
6080 // Now check if this user has already submitted etc.
6081 if (!$skipenrolled && !is_enrolled($this->get_course_context(), $userid)) {
6082 return false;
6084 // Note you can pass null for submission and it will not be fetched.
6085 if ($submission === false) {
6086 if ($this->get_instance()->teamsubmission) {
6087 $submission = $this->get_group_submission($userid, 0, false);
6088 } else {
6089 $submission = $this->get_user_submission($userid, false);
6092 if ($submission) {
6094 if ($this->get_instance()->submissiondrafts && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6095 // Drafts are tracked and the student has submitted the assignment.
6096 return false;
6100 // See if this user grade is locked in the gradebook.
6101 if ($gradinginfo === false) {
6102 $gradinginfo = grade_get_grades($this->get_course()->id,
6103 'mod',
6104 'assign',
6105 $this->get_instance()->id,
6106 array($userid));
6108 if ($gradinginfo &&
6109 isset($gradinginfo->items[0]->grades[$userid]) &&
6110 $gradinginfo->items[0]->grades[$userid]->locked) {
6111 return false;
6114 return true;
6118 * Render the files in file area.
6120 * @param string $component
6121 * @param string $area
6122 * @param int $submissionid
6123 * @return string
6125 public function render_area_files($component, $area, $submissionid) {
6126 global $USER;
6128 return $this->get_renderer()->assign_files($this->context, $submissionid, $area, $component);
6133 * Capability check to make sure this grader can edit this submission.
6135 * @param int $userid - The user whose submission is to be edited
6136 * @param int $graderid (optional) - The user who will do the editing (default to $USER->id).
6137 * @return bool
6139 public function can_edit_submission($userid, $graderid = 0) {
6140 global $USER;
6142 if (empty($graderid)) {
6143 $graderid = $USER->id;
6146 $instance = $this->get_instance();
6147 if ($userid == $graderid &&
6148 $instance->teamsubmission &&
6149 $instance->preventsubmissionnotingroup &&
6150 $this->get_submission_group($userid) == false) {
6151 return false;
6154 if ($userid == $graderid) {
6155 if ($this->submissions_open($userid) &&
6156 has_capability('mod/assign:submit', $this->context, $graderid)) {
6157 // User can edit their own submission.
6158 return true;
6159 } else {
6160 // We need to return here because editothersubmission should never apply to a users own submission.
6161 return false;
6165 if (!has_capability('mod/assign:editothersubmission', $this->context, $graderid)) {
6166 return false;
6169 $cm = $this->get_course_module();
6170 if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
6171 $sharedgroupmembers = $this->get_shared_group_members($cm, $graderid);
6172 return in_array($userid, $sharedgroupmembers);
6174 return true;
6178 * Returns IDs of the users who share group membership with the specified user.
6180 * @param stdClass|cm_info $cm Course-module
6181 * @param int $userid User ID
6182 * @return array An array of ID of users.
6184 public function get_shared_group_members($cm, $userid) {
6185 if (!isset($this->sharedgroupmembers[$userid])) {
6186 $this->sharedgroupmembers[$userid] = array();
6187 if ($members = groups_get_activity_shared_group_members($cm, $userid)) {
6188 $this->sharedgroupmembers[$userid] = array_keys($members);
6192 return $this->sharedgroupmembers[$userid];
6196 * Returns a list of teachers that should be grading given submission.
6198 * @param int $userid The submission to grade
6199 * @return array
6201 protected function get_graders($userid) {
6202 // Potential graders should be active users only.
6203 $potentialgraders = get_enrolled_users($this->context, "mod/assign:grade", null, 'u.*', null, null, null, true);
6205 $graders = array();
6206 if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
6207 if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
6208 foreach ($groups as $group) {
6209 foreach ($potentialgraders as $grader) {
6210 if ($grader->id == $userid) {
6211 // Do not send self.
6212 continue;
6214 if (groups_is_member($group->id, $grader->id)) {
6215 $graders[$grader->id] = $grader;
6219 } else {
6220 // User not in group, try to find graders without group.
6221 foreach ($potentialgraders as $grader) {
6222 if ($grader->id == $userid) {
6223 // Do not send self.
6224 continue;
6226 if (!groups_has_membership($this->get_course_module(), $grader->id)) {
6227 $graders[$grader->id] = $grader;
6231 } else {
6232 foreach ($potentialgraders as $grader) {
6233 if ($grader->id == $userid) {
6234 // Do not send self.
6235 continue;
6237 // Must be enrolled.
6238 if (is_enrolled($this->get_course_context(), $grader->id)) {
6239 $graders[$grader->id] = $grader;
6243 return $graders;
6247 * Returns a list of users that should receive notification about given submission.
6249 * @param int $userid The submission to grade
6250 * @return array
6252 protected function get_notifiable_users($userid) {
6253 // Potential users should be active users only.
6254 $potentialusers = get_enrolled_users($this->context, "mod/assign:receivegradernotifications",
6255 null, 'u.*', null, null, null, true);
6257 $notifiableusers = array();
6258 if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
6259 if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
6260 foreach ($groups as $group) {
6261 foreach ($potentialusers as $potentialuser) {
6262 if ($potentialuser->id == $userid) {
6263 // Do not send self.
6264 continue;
6266 if (groups_is_member($group->id, $potentialuser->id)) {
6267 $notifiableusers[$potentialuser->id] = $potentialuser;
6271 } else {
6272 // User not in group, try to find graders without group.
6273 foreach ($potentialusers as $potentialuser) {
6274 if ($potentialuser->id == $userid) {
6275 // Do not send self.
6276 continue;
6278 if (!groups_has_membership($this->get_course_module(), $potentialuser->id)) {
6279 $notifiableusers[$potentialuser->id] = $potentialuser;
6283 } else {
6284 foreach ($potentialusers as $potentialuser) {
6285 if ($potentialuser->id == $userid) {
6286 // Do not send self.
6287 continue;
6289 // Must be enrolled.
6290 if (is_enrolled($this->get_course_context(), $potentialuser->id)) {
6291 $notifiableusers[$potentialuser->id] = $potentialuser;
6295 return $notifiableusers;
6299 * Format a notification for plain text.
6301 * @param string $messagetype
6302 * @param stdClass $info
6303 * @param stdClass $course
6304 * @param stdClass $context
6305 * @param string $modulename
6306 * @param string $assignmentname
6308 protected static function format_notification_message_text($messagetype,
6309 $info,
6310 $course,
6311 $context,
6312 $modulename,
6313 $assignmentname) {
6314 $formatparams = array('context' => $context->get_course_context());
6315 $posttext = format_string($course->shortname, true, $formatparams) .
6316 ' -> ' .
6317 $modulename .
6318 ' -> ' .
6319 format_string($assignmentname, true, $formatparams) . "\n";
6320 $posttext .= '---------------------------------------------------------------------' . "\n";
6321 $posttext .= get_string($messagetype . 'text', 'assign', $info)."\n";
6322 $posttext .= "\n---------------------------------------------------------------------\n";
6323 return $posttext;
6327 * Format a notification for HTML.
6329 * @param string $messagetype
6330 * @param stdClass $info
6331 * @param stdClass $course
6332 * @param stdClass $context
6333 * @param string $modulename
6334 * @param stdClass $coursemodule
6335 * @param string $assignmentname
6337 protected static function format_notification_message_html($messagetype,
6338 $info,
6339 $course,
6340 $context,
6341 $modulename,
6342 $coursemodule,
6343 $assignmentname) {
6344 global $CFG;
6345 $formatparams = array('context' => $context->get_course_context());
6346 $posthtml = '<p><font face="sans-serif">' .
6347 '<a href="' . $CFG->wwwroot . '/course/view.php?id=' . $course->id . '">' .
6348 format_string($course->shortname, true, $formatparams) .
6349 '</a> ->' .
6350 '<a href="' . $CFG->wwwroot . '/mod/assign/index.php?id=' . $course->id . '">' .
6351 $modulename .
6352 '</a> ->' .
6353 '<a href="' . $CFG->wwwroot . '/mod/assign/view.php?id=' . $coursemodule->id . '">' .
6354 format_string($assignmentname, true, $formatparams) .
6355 '</a></font></p>';
6356 $posthtml .= '<hr /><font face="sans-serif">';
6357 $posthtml .= '<p>' . get_string($messagetype . 'html', 'assign', $info) . '</p>';
6358 $posthtml .= '</font><hr />';
6359 return $posthtml;
6363 * Message someone about something (static so it can be called from cron).
6365 * @param stdClass $userfrom
6366 * @param stdClass $userto
6367 * @param string $messagetype
6368 * @param string $eventtype
6369 * @param int $updatetime
6370 * @param stdClass $coursemodule
6371 * @param stdClass $context
6372 * @param stdClass $course
6373 * @param string $modulename
6374 * @param string $assignmentname
6375 * @param bool $blindmarking
6376 * @param int $uniqueidforuser
6377 * @return void
6379 public static function send_assignment_notification($userfrom,
6380 $userto,
6381 $messagetype,
6382 $eventtype,
6383 $updatetime,
6384 $coursemodule,
6385 $context,
6386 $course,
6387 $modulename,
6388 $assignmentname,
6389 $blindmarking,
6390 $uniqueidforuser) {
6391 global $CFG, $PAGE;
6393 $info = new stdClass();
6394 if ($blindmarking) {
6395 $userfrom = clone($userfrom);
6396 $info->username = get_string('participant', 'assign') . ' ' . $uniqueidforuser;
6397 $userfrom->firstname = get_string('participant', 'assign');
6398 $userfrom->lastname = $uniqueidforuser;
6399 $userfrom->email = $CFG->noreplyaddress;
6400 } else {
6401 $info->username = fullname($userfrom, true);
6403 $info->assignment = format_string($assignmentname, true, array('context'=>$context));
6404 $info->url = $CFG->wwwroot.'/mod/assign/view.php?id='.$coursemodule->id;
6405 $info->timeupdated = userdate($updatetime, get_string('strftimerecentfull'));
6407 $postsubject = get_string($messagetype . 'small', 'assign', $info);
6408 $posttext = self::format_notification_message_text($messagetype,
6409 $info,
6410 $course,
6411 $context,
6412 $modulename,
6413 $assignmentname);
6414 $posthtml = '';
6415 if ($userto->mailformat == 1) {
6416 $posthtml = self::format_notification_message_html($messagetype,
6417 $info,
6418 $course,
6419 $context,
6420 $modulename,
6421 $coursemodule,
6422 $assignmentname);
6425 $eventdata = new \core\message\message();
6426 $eventdata->courseid = $course->id;
6427 $eventdata->modulename = 'assign';
6428 $eventdata->userfrom = $userfrom;
6429 $eventdata->userto = $userto;
6430 $eventdata->subject = $postsubject;
6431 $eventdata->fullmessage = $posttext;
6432 $eventdata->fullmessageformat = FORMAT_PLAIN;
6433 $eventdata->fullmessagehtml = $posthtml;
6434 $eventdata->smallmessage = $postsubject;
6436 $eventdata->name = $eventtype;
6437 $eventdata->component = 'mod_assign';
6438 $eventdata->notification = 1;
6439 $eventdata->contexturl = $info->url;
6440 $eventdata->contexturlname = $info->assignment;
6441 $customdata = [
6442 'cmid' => $coursemodule->id,
6443 'instance' => $coursemodule->instance,
6444 'messagetype' => $messagetype,
6445 'blindmarking' => $blindmarking,
6446 'uniqueidforuser' => $uniqueidforuser,
6448 // Check if the userfrom is real and visible.
6449 if (!empty($userfrom->id) && core_user::is_real_user($userfrom->id)) {
6450 $userpicture = new user_picture($userfrom);
6451 $userpicture->size = 1; // Use f1 size.
6452 $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
6453 $customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false);
6455 $eventdata->customdata = $customdata;
6457 message_send($eventdata);
6461 * Message someone about something.
6463 * @param stdClass $userfrom
6464 * @param stdClass $userto
6465 * @param string $messagetype
6466 * @param string $eventtype
6467 * @param int $updatetime
6468 * @return void
6470 public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime) {
6471 global $USER;
6472 $userid = core_user::is_real_user($userfrom->id) ? $userfrom->id : $USER->id;
6473 $uniqueid = $this->get_uniqueid_for_user($userid);
6474 self::send_assignment_notification($userfrom,
6475 $userto,
6476 $messagetype,
6477 $eventtype,
6478 $updatetime,
6479 $this->get_course_module(),
6480 $this->get_context(),
6481 $this->get_course(),
6482 $this->get_module_name(),
6483 $this->get_instance()->name,
6484 $this->is_blind_marking(),
6485 $uniqueid);
6489 * Notify student upon successful submission copy.
6491 * @param stdClass $submission
6492 * @return void
6494 protected function notify_student_submission_copied(stdClass $submission) {
6495 global $DB, $USER;
6497 $adminconfig = $this->get_admin_config();
6498 // Use the same setting for this - no need for another one.
6499 if (empty($adminconfig->submissionreceipts)) {
6500 // No need to do anything.
6501 return;
6503 if ($submission->userid) {
6504 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6505 } else {
6506 $user = $USER;
6508 $this->send_notification($user,
6509 $user,
6510 'submissioncopied',
6511 'assign_notification',
6512 $submission->timemodified);
6515 * Notify student upon successful submission.
6517 * @param stdClass $submission
6518 * @return void
6520 protected function notify_student_submission_receipt(stdClass $submission) {
6521 global $DB, $USER;
6523 $adminconfig = $this->get_admin_config();
6524 if (empty($adminconfig->submissionreceipts)) {
6525 // No need to do anything.
6526 return;
6528 if ($submission->userid) {
6529 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6530 } else {
6531 $user = $USER;
6533 if ($submission->userid == $USER->id) {
6534 $this->send_notification(core_user::get_noreply_user(),
6535 $user,
6536 'submissionreceipt',
6537 'assign_notification',
6538 $submission->timemodified);
6539 } else {
6540 $this->send_notification($USER,
6541 $user,
6542 'submissionreceiptother',
6543 'assign_notification',
6544 $submission->timemodified);
6549 * Send notifications to graders upon student submissions.
6551 * @param stdClass $submission
6552 * @return void
6554 protected function notify_graders(stdClass $submission) {
6555 global $DB, $USER;
6557 $instance = $this->get_instance();
6559 $late = $instance->duedate && ($instance->duedate < time());
6561 if (!$instance->sendnotifications && !($late && $instance->sendlatenotifications)) {
6562 // No need to do anything.
6563 return;
6566 if ($submission->userid) {
6567 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6568 } else {
6569 $user = $USER;
6572 if ($notifyusers = $this->get_notifiable_users($user->id)) {
6573 foreach ($notifyusers as $notifyuser) {
6574 $this->send_notification($user,
6575 $notifyuser,
6576 'gradersubmissionupdated',
6577 'assign_notification',
6578 $submission->timemodified);
6584 * Submit a submission for grading.
6586 * @param stdClass $data - The form data
6587 * @param array $notices - List of error messages to display on an error condition.
6588 * @return bool Return false if the submission was not submitted.
6590 public function submit_for_grading($data, $notices) {
6591 global $USER;
6593 $userid = $USER->id;
6594 if (!empty($data->userid)) {
6595 $userid = $data->userid;
6597 // Need submit permission to submit an assignment.
6598 if ($userid == $USER->id) {
6599 require_capability('mod/assign:submit', $this->context);
6600 } else {
6601 if (!$this->can_edit_submission($userid, $USER->id)) {
6602 print_error('nopermission');
6606 $instance = $this->get_instance();
6608 if ($instance->teamsubmission) {
6609 $submission = $this->get_group_submission($userid, 0, true);
6610 } else {
6611 $submission = $this->get_user_submission($userid, true);
6614 if (!$this->submissions_open($userid)) {
6615 $notices[] = get_string('submissionsclosed', 'assign');
6616 return false;
6619 if ($instance->requiresubmissionstatement && empty($data->submissionstatement) && $USER->id == $userid) {
6620 return false;
6623 if ($submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6624 // Give each submission plugin a chance to process the submission.
6625 $plugins = $this->get_submission_plugins();
6626 foreach ($plugins as $plugin) {
6627 if ($plugin->is_enabled() && $plugin->is_visible()) {
6628 $plugin->submit_for_grading($submission);
6632 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6633 $this->update_submission($submission, $userid, true, $instance->teamsubmission);
6634 $completion = new completion_info($this->get_course());
6635 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
6636 $this->update_activity_completion_records($instance->teamsubmission,
6637 $instance->requireallteammemberssubmit,
6638 $submission,
6639 $userid,
6640 COMPLETION_COMPLETE,
6641 $completion);
6644 if (!empty($data->submissionstatement) && $USER->id == $userid) {
6645 \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
6647 $this->notify_graders($submission);
6648 $this->notify_student_submission_receipt($submission);
6650 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, false)->trigger();
6652 return true;
6654 $notices[] = get_string('submissionsclosed', 'assign');
6655 return false;
6659 * A students submission is submitted for grading by a teacher.
6661 * @return bool
6663 protected function process_submit_other_for_grading($mform, $notices) {
6664 global $USER, $CFG;
6666 require_sesskey();
6668 $userid = optional_param('userid', $USER->id, PARAM_INT);
6670 if (!$this->submissions_open($userid)) {
6671 $notices[] = get_string('submissionsclosed', 'assign');
6672 return false;
6674 $data = new stdClass();
6675 $data->userid = $userid;
6676 return $this->submit_for_grading($data, $notices);
6680 * Assignment submission is processed before grading.
6682 * @param moodleform|null $mform If validation failed when submitting this form - this is the moodleform.
6683 * It can be null.
6684 * @return bool Return false if the validation fails. This affects which page is displayed next.
6686 protected function process_submit_for_grading($mform, $notices) {
6687 global $CFG;
6689 require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
6690 require_sesskey();
6692 if (!$this->submissions_open()) {
6693 $notices[] = get_string('submissionsclosed', 'assign');
6694 return false;
6697 $data = new stdClass();
6698 $adminconfig = $this->get_admin_config();
6699 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
6701 $submissionstatement = '';
6702 if ($requiresubmissionstatement) {
6703 $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
6706 // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
6707 // that the submission statement checkbox will be displayed.
6708 if (empty($submissionstatement)) {
6709 $requiresubmissionstatement = false;
6712 if ($mform == null) {
6713 $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
6714 $submissionstatement,
6715 $this->get_course_module()->id,
6716 $data));
6719 $data = $mform->get_data();
6720 if (!$mform->is_cancelled()) {
6721 if ($mform->get_data() == false) {
6722 return false;
6724 return $this->submit_for_grading($data, $notices);
6726 return true;
6730 * Save the extension date for a single user.
6732 * @param int $userid The user id
6733 * @param mixed $extensionduedate Either an integer date or null
6734 * @return boolean
6736 public function save_user_extension($userid, $extensionduedate) {
6737 global $DB;
6739 // Need submit permission to submit an assignment.
6740 require_capability('mod/assign:grantextension', $this->context);
6742 if (!is_enrolled($this->get_course_context(), $userid)) {
6743 return false;
6745 if (!has_capability('mod/assign:submit', $this->context, $userid)) {
6746 return false;
6749 if ($this->get_instance()->duedate && $extensionduedate) {
6750 if ($this->get_instance()->duedate > $extensionduedate) {
6751 return false;
6754 if ($this->get_instance()->allowsubmissionsfromdate && $extensionduedate) {
6755 if ($this->get_instance()->allowsubmissionsfromdate > $extensionduedate) {
6756 return false;
6760 $flags = $this->get_user_flags($userid, true);
6761 $flags->extensionduedate = $extensionduedate;
6763 $result = $this->update_user_flags($flags);
6765 if ($result) {
6766 \mod_assign\event\extension_granted::create_from_assign($this, $userid)->trigger();
6768 return $result;
6772 * Save extension date.
6774 * @param moodleform $mform The submitted form
6775 * @return boolean
6777 protected function process_save_extension(& $mform) {
6778 global $DB, $CFG;
6780 // Include extension form.
6781 require_once($CFG->dirroot . '/mod/assign/extensionform.php');
6782 require_sesskey();
6784 $users = optional_param('userid', 0, PARAM_INT);
6785 if (!$users) {
6786 $users = required_param('selectedusers', PARAM_SEQUENCE);
6788 $userlist = explode(',', $users);
6790 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
6791 $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
6792 foreach ($userlist as $userid) {
6793 // To validate extension date with users overrides.
6794 $override = $this->override_exists($userid);
6795 foreach ($keys as $key) {
6796 if ($override->{$key}) {
6797 if ($maxoverride[$key] < $override->{$key}) {
6798 $maxoverride[$key] = $override->{$key};
6800 } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
6801 $maxoverride[$key] = $this->get_instance()->{$key};
6805 foreach ($keys as $key) {
6806 if ($maxoverride[$key]) {
6807 $this->get_instance()->{$key} = $maxoverride[$key];
6811 $formparams = array(
6812 'instance' => $this->get_instance(),
6813 'assign' => $this,
6814 'userlist' => $userlist
6817 $mform = new mod_assign_extension_form(null, $formparams);
6819 if ($mform->is_cancelled()) {
6820 return true;
6823 if ($formdata = $mform->get_data()) {
6824 if (!empty($formdata->selectedusers)) {
6825 $users = explode(',', $formdata->selectedusers);
6826 $result = true;
6827 foreach ($users as $userid) {
6828 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
6829 $result = $this->save_user_extension($user->id, $formdata->extensionduedate) && $result;
6831 return $result;
6833 if (!empty($formdata->userid)) {
6834 $user = $DB->get_record('user', array('id' => $formdata->userid), '*', MUST_EXIST);
6835 return $this->save_user_extension($user->id, $formdata->extensionduedate);
6839 return false;
6843 * Save quick grades.
6845 * @return string The result of the save operation
6847 protected function process_save_quick_grades() {
6848 global $USER, $DB, $CFG;
6850 // Need grade permission.
6851 require_capability('mod/assign:grade', $this->context);
6852 require_sesskey();
6854 // Make sure advanced grading is disabled.
6855 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
6856 $controller = $gradingmanager->get_active_controller();
6857 if (!empty($controller)) {
6858 $message = get_string('errorquickgradingvsadvancedgrading', 'assign');
6859 $this->set_error_message($message);
6860 return $message;
6863 $users = array();
6864 // First check all the last modified values.
6865 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
6866 $participants = $this->list_participants($currentgroup, true);
6868 // Gets a list of possible users and look for values based upon that.
6869 foreach ($participants as $userid => $unused) {
6870 $modified = optional_param('grademodified_' . $userid, -1, PARAM_INT);
6871 $attemptnumber = optional_param('gradeattempt_' . $userid, -1, PARAM_INT);
6872 // Gather the userid, updated grade and last modified value.
6873 $record = new stdClass();
6874 $record->userid = $userid;
6875 if ($modified >= 0) {
6876 $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT));
6877 $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_ALPHA);
6878 $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT);
6879 } else {
6880 // This user was not in the grading table.
6881 continue;
6883 $record->attemptnumber = $attemptnumber;
6884 $record->lastmodified = $modified;
6885 $record->gradinginfo = grade_get_grades($this->get_course()->id,
6886 'mod',
6887 'assign',
6888 $this->get_instance()->id,
6889 array($userid));
6890 $users[$userid] = $record;
6893 if (empty($users)) {
6894 $message = get_string('nousersselected', 'assign');
6895 $this->set_error_message($message);
6896 return $message;
6899 list($userids, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED);
6900 $params['assignid1'] = $this->get_instance()->id;
6901 $params['assignid2'] = $this->get_instance()->id;
6903 // Check them all for currency.
6904 $grademaxattempt = 'SELECT s.userid, s.attemptnumber AS maxattempt
6905 FROM {assign_submission} s
6906 WHERE s.assignment = :assignid1 AND s.latest = 1';
6908 $sql = 'SELECT u.id AS userid, g.grade AS grade, g.timemodified AS lastmodified,
6909 uf.workflowstate, uf.allocatedmarker, gmx.maxattempt AS attemptnumber
6910 FROM {user} u
6911 LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
6912 LEFT JOIN {assign_grades} g ON
6913 u.id = g.userid AND
6914 g.assignment = :assignid2 AND
6915 g.attemptnumber = gmx.maxattempt
6916 LEFT JOIN {assign_user_flags} uf ON uf.assignment = g.assignment AND uf.userid = g.userid
6917 WHERE u.id ' . $userids;
6918 $currentgrades = $DB->get_recordset_sql($sql, $params);
6920 $modifiedusers = array();
6921 foreach ($currentgrades as $current) {
6922 $modified = $users[(int)$current->userid];
6923 $grade = $this->get_user_grade($modified->userid, false);
6924 // Check to see if the grade column was even visible.
6925 $gradecolpresent = optional_param('quickgrade_' . $modified->userid, false, PARAM_INT) !== false;
6927 // Check to see if the outcomes were modified.
6928 if ($CFG->enableoutcomes) {
6929 foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
6930 $oldoutcome = $outcome->grades[$modified->userid]->grade;
6931 $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
6932 $newoutcome = optional_param($paramname, -1, PARAM_FLOAT);
6933 // Check to see if the outcome column was even visible.
6934 $outcomecolpresent = optional_param($paramname, false, PARAM_FLOAT) !== false;
6935 if ($outcomecolpresent && ($oldoutcome != $newoutcome)) {
6936 // Can't check modified time for outcomes because it is not reported.
6937 $modifiedusers[$modified->userid] = $modified;
6938 continue;
6943 // Let plugins participate.
6944 foreach ($this->feedbackplugins as $plugin) {
6945 if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
6946 // The plugins must handle is_quickgrading_modified correctly - ie
6947 // handle hidden columns.
6948 if ($plugin->is_quickgrading_modified($modified->userid, $grade)) {
6949 if ((int)$current->lastmodified > (int)$modified->lastmodified) {
6950 $message = get_string('errorrecordmodified', 'assign');
6951 $this->set_error_message($message);
6952 return $message;
6953 } else {
6954 $modifiedusers[$modified->userid] = $modified;
6955 continue;
6961 if (($current->grade < 0 || $current->grade === null) &&
6962 ($modified->grade < 0 || $modified->grade === null)) {
6963 // Different ways to indicate no grade.
6964 $modified->grade = $current->grade; // Keep existing grade.
6966 // Treat 0 and null as different values.
6967 if ($current->grade !== null) {
6968 $current->grade = floatval($current->grade);
6970 $gradechanged = $gradecolpresent && grade_floats_different($current->grade, $modified->grade);
6971 $markingallocationchanged = $this->get_instance()->markingworkflow &&
6972 $this->get_instance()->markingallocation &&
6973 ($modified->allocatedmarker !== false) &&
6974 ($current->allocatedmarker != $modified->allocatedmarker);
6975 $workflowstatechanged = $this->get_instance()->markingworkflow &&
6976 ($modified->workflowstate !== false) &&
6977 ($current->workflowstate != $modified->workflowstate);
6978 if ($gradechanged || $markingallocationchanged || $workflowstatechanged) {
6979 // Grade changed.
6980 if ($this->grading_disabled($modified->userid)) {
6981 continue;
6983 $badmodified = (int)$current->lastmodified > (int)$modified->lastmodified;
6984 $badattempt = (int)$current->attemptnumber != (int)$modified->attemptnumber;
6985 if ($badmodified || $badattempt) {
6986 // Error - record has been modified since viewing the page.
6987 $message = get_string('errorrecordmodified', 'assign');
6988 $this->set_error_message($message);
6989 return $message;
6990 } else {
6991 $modifiedusers[$modified->userid] = $modified;
6996 $currentgrades->close();
6998 $adminconfig = $this->get_admin_config();
6999 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7001 // Ok - ready to process the updates.
7002 foreach ($modifiedusers as $userid => $modified) {
7003 $grade = $this->get_user_grade($userid, true);
7004 $flags = $this->get_user_flags($userid, true);
7005 $grade->grade= grade_floatval(unformat_float($modified->grade));
7006 $grade->grader= $USER->id;
7007 $gradecolpresent = optional_param('quickgrade_' . $userid, false, PARAM_INT) !== false;
7009 // Save plugins data.
7010 foreach ($this->feedbackplugins as $plugin) {
7011 if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
7012 $plugin->save_quickgrading_changes($userid, $grade);
7013 if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
7014 // This is the feedback plugin chose to push comments to the gradebook.
7015 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
7016 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
7017 $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
7022 // These will be set to false if they are not present in the quickgrading
7023 // form (e.g. column hidden).
7024 $workflowstatemodified = ($modified->workflowstate !== false) &&
7025 ($flags->workflowstate != $modified->workflowstate);
7027 $allocatedmarkermodified = ($modified->allocatedmarker !== false) &&
7028 ($flags->allocatedmarker != $modified->allocatedmarker);
7030 if ($workflowstatemodified) {
7031 $flags->workflowstate = $modified->workflowstate;
7033 if ($allocatedmarkermodified) {
7034 $flags->allocatedmarker = $modified->allocatedmarker;
7036 if ($workflowstatemodified || $allocatedmarkermodified) {
7037 if ($this->update_user_flags($flags) && $workflowstatemodified) {
7038 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7039 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $flags->workflowstate)->trigger();
7042 $this->update_grade($grade);
7044 // Allow teachers to skip sending notifications.
7045 if (optional_param('sendstudentnotifications', true, PARAM_BOOL)) {
7046 $this->notify_grade_modified($grade, true);
7049 // Save outcomes.
7050 if ($CFG->enableoutcomes) {
7051 $data = array();
7052 foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
7053 $oldoutcome = $outcome->grades[$modified->userid]->grade;
7054 $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
7055 // This will be false if the input was not in the quickgrading
7056 // form (e.g. column hidden).
7057 $newoutcome = optional_param($paramname, false, PARAM_INT);
7058 if ($newoutcome !== false && ($oldoutcome != $newoutcome)) {
7059 $data[$outcomeid] = $newoutcome;
7062 if (count($data) > 0) {
7063 grade_update_outcomes('mod/assign',
7064 $this->course->id,
7065 'mod',
7066 'assign',
7067 $this->get_instance()->id,
7068 $userid,
7069 $data);
7074 return get_string('quickgradingchangessaved', 'assign');
7078 * Reveal student identities to markers (and the gradebook).
7080 * @return void
7082 public function reveal_identities() {
7083 global $DB;
7085 require_capability('mod/assign:revealidentities', $this->context);
7087 if ($this->get_instance()->revealidentities || empty($this->get_instance()->blindmarking)) {
7088 return false;
7091 // Update the assignment record.
7092 $update = new stdClass();
7093 $update->id = $this->get_instance()->id;
7094 $update->revealidentities = 1;
7095 $DB->update_record('assign', $update);
7097 // Refresh the instance data.
7098 $this->instance = null;
7100 // Release the grades to the gradebook.
7101 // First create the column in the gradebook.
7102 $this->update_gradebook(false, $this->get_course_module()->id);
7104 // Now release all grades.
7106 $adminconfig = $this->get_admin_config();
7107 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7108 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
7109 $grades = $DB->get_records('assign_grades', array('assignment'=>$this->get_instance()->id));
7111 $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
7113 foreach ($grades as $grade) {
7114 // Fetch any comments for this student.
7115 if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
7116 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
7117 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
7118 $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
7120 $this->gradebook_item_update(null, $grade);
7123 \mod_assign\event\identities_revealed::create_from_assign($this)->trigger();
7127 * Reveal student identities to markers (and the gradebook).
7129 * @return void
7131 protected function process_reveal_identities() {
7133 if (!confirm_sesskey()) {
7134 return false;
7137 return $this->reveal_identities();
7142 * Save grading options.
7144 * @return void
7146 protected function process_save_grading_options() {
7147 global $USER, $CFG;
7149 // Include grading options form.
7150 require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
7152 // Need submit permission to submit an assignment.
7153 $this->require_view_grades();
7154 require_sesskey();
7156 // Is advanced grading enabled?
7157 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
7158 $controller = $gradingmanager->get_active_controller();
7159 $showquickgrading = empty($controller);
7160 if (!is_null($this->context)) {
7161 $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
7162 } else {
7163 $showonlyactiveenrolopt = false;
7166 $markingallocation = $this->get_instance()->markingworkflow &&
7167 $this->get_instance()->markingallocation &&
7168 has_capability('mod/assign:manageallocations', $this->context);
7169 // Get markers to use in drop lists.
7170 $markingallocationoptions = array();
7171 if ($markingallocation) {
7172 $markingallocationoptions[''] = get_string('filternone', 'assign');
7173 $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
7174 list($sort, $params) = users_order_by_sql('u');
7175 // Only enrolled users could be assigned as potential markers.
7176 $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
7177 foreach ($markers as $marker) {
7178 $markingallocationoptions[$marker->id] = fullname($marker);
7182 // Get marking states to show in form.
7183 $markingworkflowoptions = $this->get_marking_workflow_filters();
7185 $gradingoptionsparams = array('cm'=>$this->get_course_module()->id,
7186 'contextid'=>$this->context->id,
7187 'userid'=>$USER->id,
7188 'submissionsenabled'=>$this->is_any_submission_plugin_enabled(),
7189 'showquickgrading'=>$showquickgrading,
7190 'quickgrading'=>false,
7191 'markingworkflowopt' => $markingworkflowoptions,
7192 'markingallocationopt' => $markingallocationoptions,
7193 'showonlyactiveenrolopt'=>$showonlyactiveenrolopt,
7194 'showonlyactiveenrol' => $this->show_only_active_users(),
7195 'downloadasfolders' => get_user_preferences('assign_downloadasfolders', 1));
7196 $mform = new mod_assign_grading_options_form(null, $gradingoptionsparams);
7197 if ($formdata = $mform->get_data()) {
7198 set_user_preference('assign_perpage', $formdata->perpage);
7199 if (isset($formdata->filter)) {
7200 set_user_preference('assign_filter', $formdata->filter);
7202 if (isset($formdata->markerfilter)) {
7203 set_user_preference('assign_markerfilter', $formdata->markerfilter);
7205 if (isset($formdata->workflowfilter)) {
7206 set_user_preference('assign_workflowfilter', $formdata->workflowfilter);
7208 if ($showquickgrading) {
7209 set_user_preference('assign_quickgrading', isset($formdata->quickgrading));
7211 if (isset($formdata->downloadasfolders)) {
7212 set_user_preference('assign_downloadasfolders', 1); // Enabled.
7213 } else {
7214 set_user_preference('assign_downloadasfolders', 0); // Disabled.
7216 if (!empty($showonlyactiveenrolopt)) {
7217 $showonlyactiveenrol = isset($formdata->showonlyactiveenrol);
7218 set_user_preference('grade_report_showonlyactiveenrol', $showonlyactiveenrol);
7219 $this->showonlyactiveenrol = $showonlyactiveenrol;
7225 * Take a grade object and print a short summary for the log file.
7226 * The size limit for the log file is 255 characters, so be careful not
7227 * to include too much information.
7229 * @deprecated since 2.7
7231 * @param stdClass $grade
7232 * @return string
7234 public function format_grade_for_log(stdClass $grade) {
7235 global $DB;
7237 $user = $DB->get_record('user', array('id' => $grade->userid), '*', MUST_EXIST);
7239 $info = get_string('gradestudent', 'assign', array('id'=>$user->id, 'fullname'=>fullname($user)));
7240 if ($grade->grade != '') {
7241 $info .= get_string('gradenoun') . ': ' . $this->display_grade($grade->grade, false) . '. ';
7242 } else {
7243 $info .= get_string('nograde', 'assign');
7245 return $info;
7249 * Take a submission object and print a short summary for the log file.
7250 * The size limit for the log file is 255 characters, so be careful not
7251 * to include too much information.
7253 * @deprecated since 2.7
7255 * @param stdClass $submission
7256 * @return string
7258 public function format_submission_for_log(stdClass $submission) {
7259 global $DB;
7261 $info = '';
7262 if ($submission->userid) {
7263 $user = $DB->get_record('user', array('id' => $submission->userid), '*', MUST_EXIST);
7264 $name = fullname($user);
7265 } else {
7266 $group = $this->get_submission_group($submission->userid);
7267 if ($group) {
7268 $name = $group->name;
7269 } else {
7270 $name = get_string('defaultteam', 'assign');
7273 $status = get_string('submissionstatus_' . $submission->status, 'assign');
7274 $params = array('id'=>$submission->userid, 'fullname'=>$name, 'status'=>$status);
7275 $info .= get_string('submissionlog', 'assign', $params) . ' <br>';
7277 foreach ($this->submissionplugins as $plugin) {
7278 if ($plugin->is_enabled() && $plugin->is_visible()) {
7279 $info .= '<br>' . $plugin->format_for_log($submission);
7283 return $info;
7287 * Require a valid sess key and then call copy_previous_attempt.
7289 * @param array $notices Any error messages that should be shown
7290 * to the user at the top of the edit submission form.
7291 * @return bool
7293 protected function process_copy_previous_attempt(&$notices) {
7294 require_sesskey();
7296 return $this->copy_previous_attempt($notices);
7300 * Copy the current assignment submission from the last submitted attempt.
7302 * @param array $notices Any error messages that should be shown
7303 * to the user at the top of the edit submission form.
7304 * @return bool
7306 public function copy_previous_attempt(&$notices) {
7307 global $USER, $CFG;
7309 require_capability('mod/assign:submit', $this->context);
7311 $instance = $this->get_instance();
7312 if ($instance->teamsubmission) {
7313 $submission = $this->get_group_submission($USER->id, 0, true);
7314 } else {
7315 $submission = $this->get_user_submission($USER->id, true);
7317 if (!$submission || $submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
7318 $notices[] = get_string('submissionnotcopiedinvalidstatus', 'assign');
7319 return false;
7321 $flags = $this->get_user_flags($USER->id, false);
7323 // Get the flags to check if it is locked.
7324 if ($flags && $flags->locked) {
7325 $notices[] = get_string('submissionslocked', 'assign');
7326 return false;
7328 if ($instance->submissiondrafts) {
7329 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
7330 } else {
7331 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
7333 $this->update_submission($submission, $USER->id, true, $instance->teamsubmission);
7335 // Find the previous submission.
7336 if ($instance->teamsubmission) {
7337 $previoussubmission = $this->get_group_submission($USER->id, 0, true, $submission->attemptnumber - 1);
7338 } else {
7339 $previoussubmission = $this->get_user_submission($USER->id, true, $submission->attemptnumber - 1);
7342 if (!$previoussubmission) {
7343 // There was no previous submission so there is nothing else to do.
7344 return true;
7347 $pluginerror = false;
7348 foreach ($this->get_submission_plugins() as $plugin) {
7349 if ($plugin->is_visible() && $plugin->is_enabled()) {
7350 if (!$plugin->copy_submission($previoussubmission, $submission)) {
7351 $notices[] = $plugin->get_error();
7352 $pluginerror = true;
7356 if ($pluginerror) {
7357 return false;
7360 \mod_assign\event\submission_duplicated::create_from_submission($this, $submission)->trigger();
7362 $complete = COMPLETION_INCOMPLETE;
7363 if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
7364 $complete = COMPLETION_COMPLETE;
7366 $completion = new completion_info($this->get_course());
7367 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
7368 $this->update_activity_completion_records($instance->teamsubmission,
7369 $instance->requireallteammemberssubmit,
7370 $submission,
7371 $USER->id,
7372 $complete,
7373 $completion);
7376 if (!$instance->submissiondrafts) {
7377 // There is a case for not notifying the student about the submission copy,
7378 // but it provides a record of the event and if they then cancel editing it
7379 // is clear that the submission was copied.
7380 $this->notify_student_submission_copied($submission);
7381 $this->notify_graders($submission);
7383 // The same logic applies here - we could not notify teachers,
7384 // but then they would wonder why there are submitted assignments
7385 // and they haven't been notified.
7386 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
7388 return true;
7392 * Determine if the current submission is empty or not.
7394 * @param submission $submission the students submission record to check.
7395 * @return bool
7397 public function submission_empty($submission) {
7398 $allempty = true;
7400 foreach ($this->submissionplugins as $plugin) {
7401 if ($plugin->is_enabled() && $plugin->is_visible()) {
7402 if (!$allempty || !$plugin->is_empty($submission)) {
7403 $allempty = false;
7407 return $allempty;
7411 * Determine if a new submission is empty or not
7413 * @param stdClass $data Submission data
7414 * @return bool
7416 public function new_submission_empty($data) {
7417 foreach ($this->submissionplugins as $plugin) {
7418 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions() &&
7419 !$plugin->submission_is_empty($data)) {
7420 return false;
7423 return true;
7427 * Save assignment submission for the current user.
7429 * @param stdClass $data
7430 * @param array $notices Any error messages that should be shown
7431 * to the user.
7432 * @return bool
7434 public function save_submission(stdClass $data, & $notices) {
7435 global $CFG, $USER, $DB;
7437 $userid = $USER->id;
7438 if (!empty($data->userid)) {
7439 $userid = $data->userid;
7442 $user = clone($USER);
7443 if ($userid == $USER->id) {
7444 require_capability('mod/assign:submit', $this->context);
7445 } else {
7446 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
7447 if (!$this->can_edit_submission($userid, $USER->id)) {
7448 print_error('nopermission');
7451 $instance = $this->get_instance();
7453 if ($instance->teamsubmission) {
7454 $submission = $this->get_group_submission($userid, 0, true);
7455 } else {
7456 $submission = $this->get_user_submission($userid, true);
7459 if ($this->new_submission_empty($data)) {
7460 $notices[] = get_string('submissionempty', 'mod_assign');
7461 return false;
7464 // Check that no one has modified the submission since we started looking at it.
7465 if (isset($data->lastmodified) && ($submission->timemodified > $data->lastmodified)) {
7466 // Another user has submitted something. Notify the current user.
7467 if ($submission->status !== ASSIGN_SUBMISSION_STATUS_NEW) {
7468 $notices[] = $instance->teamsubmission ? get_string('submissionmodifiedgroup', 'mod_assign')
7469 : get_string('submissionmodified', 'mod_assign');
7470 return false;
7474 if ($instance->submissiondrafts) {
7475 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
7476 } else {
7477 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
7480 $flags = $this->get_user_flags($userid, false);
7482 // Get the flags to check if it is locked.
7483 if ($flags && $flags->locked) {
7484 print_error('submissionslocked', 'assign');
7485 return true;
7488 $pluginerror = false;
7489 foreach ($this->submissionplugins as $plugin) {
7490 if ($plugin->is_enabled() && $plugin->is_visible()) {
7491 if (!$plugin->save($submission, $data)) {
7492 $notices[] = $plugin->get_error();
7493 $pluginerror = true;
7498 $allempty = $this->submission_empty($submission);
7499 if ($pluginerror || $allempty) {
7500 if ($allempty) {
7501 $notices[] = get_string('submissionempty', 'mod_assign');
7503 return false;
7506 $this->update_submission($submission, $userid, true, $instance->teamsubmission);
7507 $users = [$userid];
7509 if ($instance->teamsubmission && !$instance->requireallteammemberssubmit) {
7510 $team = $this->get_submission_group_members($submission->groupid, true);
7512 foreach ($team as $member) {
7513 if ($member->id != $userid) {
7514 $membersubmission = clone($submission);
7515 $this->update_submission($membersubmission, $member->id, true, $instance->teamsubmission);
7516 $users[] = $member->id;
7521 $complete = COMPLETION_INCOMPLETE;
7522 if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
7523 $complete = COMPLETION_COMPLETE;
7526 $completion = new completion_info($this->get_course());
7527 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
7528 foreach ($users as $id) {
7529 $completion->update_state($this->get_course_module(), $complete, $id);
7533 // Logging.
7534 if (isset($data->submissionstatement) && ($userid == $USER->id)) {
7535 \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
7538 if (!$instance->submissiondrafts) {
7539 $this->notify_student_submission_receipt($submission);
7540 $this->notify_graders($submission);
7541 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
7543 return true;
7547 * Save assignment submission.
7549 * @param moodleform $mform
7550 * @param array $notices Any error messages that should be shown
7551 * to the user at the top of the edit submission form.
7552 * @return bool
7554 protected function process_save_submission(&$mform, &$notices) {
7555 global $CFG, $USER;
7557 // Include submission form.
7558 require_once($CFG->dirroot . '/mod/assign/submission_form.php');
7560 $userid = optional_param('userid', $USER->id, PARAM_INT);
7561 // Need submit permission to submit an assignment.
7562 require_sesskey();
7563 if (!$this->submissions_open($userid)) {
7564 $notices[] = get_string('duedatereached', 'assign');
7565 return false;
7567 $instance = $this->get_instance();
7569 $data = new stdClass();
7570 $data->userid = $userid;
7571 $mform = new mod_assign_submission_form(null, array($this, $data));
7572 if ($mform->is_cancelled()) {
7573 return true;
7575 if ($data = $mform->get_data()) {
7576 return $this->save_submission($data, $notices);
7578 return false;
7583 * Determine if this users grade can be edited.
7585 * @param int $userid - The student userid
7586 * @param bool $checkworkflow - whether to include a check for the workflow state.
7587 * @return bool $gradingdisabled
7589 public function grading_disabled($userid, $checkworkflow=true) {
7590 global $CFG;
7591 if ($checkworkflow && $this->get_instance()->markingworkflow) {
7592 $grade = $this->get_user_grade($userid, false);
7593 $validstates = $this->get_marking_workflow_states_for_current_user();
7594 if (!empty($grade) && !empty($grade->workflowstate) && !array_key_exists($grade->workflowstate, $validstates)) {
7595 return true;
7598 $gradinginfo = grade_get_grades($this->get_course()->id,
7599 'mod',
7600 'assign',
7601 $this->get_instance()->id,
7602 array($userid));
7603 if (!$gradinginfo) {
7604 return false;
7607 if (!isset($gradinginfo->items[0]->grades[$userid])) {
7608 return false;
7610 $gradingdisabled = $gradinginfo->items[0]->grades[$userid]->locked ||
7611 $gradinginfo->items[0]->grades[$userid]->overridden;
7612 return $gradingdisabled;
7617 * Get an instance of a grading form if advanced grading is enabled.
7618 * This is specific to the assignment, marker and student.
7620 * @param int $userid - The student userid
7621 * @param stdClass|false $grade - The grade record
7622 * @param bool $gradingdisabled
7623 * @return mixed gradingform_instance|null $gradinginstance
7625 protected function get_grading_instance($userid, $grade, $gradingdisabled) {
7626 global $CFG, $USER;
7628 $grademenu = make_grades_menu($this->get_instance()->grade);
7629 $allowgradedecimals = $this->get_instance()->grade > 0;
7631 $advancedgradingwarning = false;
7632 $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
7633 $gradinginstance = null;
7634 if ($gradingmethod = $gradingmanager->get_active_method()) {
7635 $controller = $gradingmanager->get_controller($gradingmethod);
7636 if ($controller->is_form_available()) {
7637 $itemid = null;
7638 if ($grade) {
7639 $itemid = $grade->id;
7641 if ($gradingdisabled && $itemid) {
7642 $gradinginstance = $controller->get_current_instance($USER->id, $itemid);
7643 } else if (!$gradingdisabled) {
7644 $instanceid = optional_param('advancedgradinginstanceid', 0, PARAM_INT);
7645 $gradinginstance = $controller->get_or_create_instance($instanceid,
7646 $USER->id,
7647 $itemid);
7649 } else {
7650 $advancedgradingwarning = $controller->form_unavailable_notification();
7653 if ($gradinginstance) {
7654 $gradinginstance->get_controller()->set_grade_range($grademenu, $allowgradedecimals);
7656 return $gradinginstance;
7660 * Add elements to grade form.
7662 * @param MoodleQuickForm $mform
7663 * @param stdClass $data
7664 * @param array $params
7665 * @return void
7667 public function add_grade_form_elements(MoodleQuickForm $mform, stdClass $data, $params) {
7668 global $USER, $CFG, $SESSION;
7669 $settings = $this->get_instance();
7671 $rownum = isset($params['rownum']) ? $params['rownum'] : 0;
7672 $last = isset($params['last']) ? $params['last'] : true;
7673 $useridlistid = isset($params['useridlistid']) ? $params['useridlistid'] : 0;
7674 $userid = isset($params['userid']) ? $params['userid'] : 0;
7675 $attemptnumber = isset($params['attemptnumber']) ? $params['attemptnumber'] : 0;
7676 $gradingpanel = !empty($params['gradingpanel']);
7677 $bothids = ($userid && $useridlistid);
7679 if (!$userid || $bothids) {
7680 $useridlist = $this->get_grading_userid_list(true, $useridlistid);
7681 } else {
7682 $useridlist = array($userid);
7683 $rownum = 0;
7684 $useridlistid = '';
7687 $userid = $useridlist[$rownum];
7688 // We need to create a grade record matching this attempt number
7689 // or the feedback plugin will have no way to know what is the correct attempt.
7690 $grade = $this->get_user_grade($userid, true, $attemptnumber);
7692 $submission = null;
7693 if ($this->get_instance()->teamsubmission) {
7694 $submission = $this->get_group_submission($userid, 0, false, $attemptnumber);
7695 } else {
7696 $submission = $this->get_user_submission($userid, false, $attemptnumber);
7699 // Add advanced grading.
7700 $gradingdisabled = $this->grading_disabled($userid);
7701 $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
7703 $mform->addElement('header', 'gradeheader', get_string('gradenoun'));
7704 if ($gradinginstance) {
7705 $gradingelement = $mform->addElement('grading',
7706 'advancedgrading',
7707 get_string('gradenoun') . ':',
7708 array('gradinginstance' => $gradinginstance));
7709 if ($gradingdisabled) {
7710 $gradingelement->freeze();
7711 } else {
7712 $mform->addElement('hidden', 'advancedgradinginstanceid', $gradinginstance->get_id());
7713 $mform->setType('advancedgradinginstanceid', PARAM_INT);
7715 } else {
7716 // Use simple direct grading.
7717 if ($this->get_instance()->grade > 0) {
7718 $name = get_string('gradeoutof', 'assign', $this->get_instance()->grade);
7719 if (!$gradingdisabled) {
7720 $gradingelement = $mform->addElement('text', 'grade', $name);
7721 $mform->addHelpButton('grade', 'gradeoutofhelp', 'assign');
7722 $mform->setType('grade', PARAM_RAW);
7723 } else {
7724 $strgradelocked = get_string('gradelocked', 'assign');
7725 $mform->addElement('static', 'gradedisabled', $name, $strgradelocked);
7726 $mform->addHelpButton('gradedisabled', 'gradeoutofhelp', 'assign');
7728 } else {
7729 $grademenu = array(-1 => get_string("nograde")) + make_grades_menu($this->get_instance()->grade);
7730 if (count($grademenu) > 1) {
7731 $gradingelement = $mform->addElement('select', 'grade', get_string('gradenoun') . ':', $grademenu);
7733 // The grade is already formatted with format_float so it needs to be converted back to an integer.
7734 if (!empty($data->grade)) {
7735 $data->grade = (int)unformat_float($data->grade);
7737 $mform->setType('grade', PARAM_INT);
7738 if ($gradingdisabled) {
7739 $gradingelement->freeze();
7745 $gradinginfo = grade_get_grades($this->get_course()->id,
7746 'mod',
7747 'assign',
7748 $this->get_instance()->id,
7749 $userid);
7750 if (!empty($CFG->enableoutcomes)) {
7751 foreach ($gradinginfo->outcomes as $index => $outcome) {
7752 $options = make_grades_menu(-$outcome->scaleid);
7753 $options[0] = get_string('nooutcome', 'grades');
7754 if ($outcome->grades[$userid]->locked) {
7755 $mform->addElement('static',
7756 'outcome_' . $index . '[' . $userid . ']',
7757 $outcome->name . ':',
7758 $options[$outcome->grades[$userid]->grade]);
7759 } else {
7760 $attributes = array('id' => 'menuoutcome_' . $index );
7761 $mform->addElement('select',
7762 'outcome_' . $index . '[' . $userid . ']',
7763 $outcome->name.':',
7764 $options,
7765 $attributes);
7766 $mform->setType('outcome_' . $index . '[' . $userid . ']', PARAM_INT);
7767 $mform->setDefault('outcome_' . $index . '[' . $userid . ']',
7768 $outcome->grades[$userid]->grade);
7773 $capabilitylist = array('gradereport/grader:view', 'moodle/grade:viewall');
7774 if (has_all_capabilities($capabilitylist, $this->get_course_context())) {
7775 $urlparams = array('id'=>$this->get_course()->id);
7776 $url = new moodle_url('/grade/report/grader/index.php', $urlparams);
7777 $usergrade = '-';
7778 if (isset($gradinginfo->items[0]->grades[$userid]->str_grade)) {
7779 $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
7781 $gradestring = $this->get_renderer()->action_link($url, $usergrade);
7782 } else {
7783 $usergrade = '-';
7784 if (isset($gradinginfo->items[0]->grades[$userid]) &&
7785 !$gradinginfo->items[0]->grades[$userid]->hidden) {
7786 $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
7788 $gradestring = $usergrade;
7791 if ($this->get_instance()->markingworkflow) {
7792 $states = $this->get_marking_workflow_states_for_current_user();
7793 $options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states;
7794 $mform->addElement('select', 'workflowstate', get_string('markingworkflowstate', 'assign'), $options);
7795 $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign');
7796 $gradingstatus = $this->get_grading_status($userid);
7797 if ($gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
7798 if ($grade->grade && $grade->grade != -1) {
7799 $assigngradestring = html_writer::span(
7800 make_grades_menu($settings->grade)[grade_floatval($grade->grade)], 'currentgrade'
7802 $label = get_string('currentassigngrade', 'assign');
7803 $mform->addElement('static', 'currentassigngrade', $label, $assigngradestring);
7808 if ($this->get_instance()->markingworkflow &&
7809 $this->get_instance()->markingallocation &&
7810 has_capability('mod/assign:manageallocations', $this->context)) {
7812 list($sort, $params) = users_order_by_sql('u');
7813 // Only enrolled users could be assigned as potential markers.
7814 $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
7815 $markerlist = array('' => get_string('choosemarker', 'assign'));
7816 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
7817 foreach ($markers as $marker) {
7818 $markerlist[$marker->id] = fullname($marker, $viewfullnames);
7820 $mform->addElement('select', 'allocatedmarker', get_string('allocatedmarker', 'assign'), $markerlist);
7821 $mform->addHelpButton('allocatedmarker', 'allocatedmarker', 'assign');
7822 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW);
7823 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW);
7824 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE);
7825 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
7828 $gradestring = '<span class="currentgrade">' . $gradestring . '</span>';
7829 $mform->addElement('static', 'currentgrade', get_string('currentgrade', 'assign'), $gradestring);
7831 if (count($useridlist) > 1) {
7832 $strparams = array('current'=>$rownum+1, 'total'=>count($useridlist));
7833 $name = get_string('outof', 'assign', $strparams);
7834 $mform->addElement('static', 'gradingstudent', get_string('gradingstudent', 'assign'), $name);
7837 // Let feedback plugins add elements to the grading form.
7838 $this->add_plugin_grade_elements($grade, $mform, $data, $userid);
7840 // Hidden params.
7841 $mform->addElement('hidden', 'id', $this->get_course_module()->id);
7842 $mform->setType('id', PARAM_INT);
7843 $mform->addElement('hidden', 'rownum', $rownum);
7844 $mform->setType('rownum', PARAM_INT);
7845 $mform->setConstant('rownum', $rownum);
7846 $mform->addElement('hidden', 'useridlistid', $useridlistid);
7847 $mform->setType('useridlistid', PARAM_ALPHANUM);
7848 $mform->addElement('hidden', 'attemptnumber', $attemptnumber);
7849 $mform->setType('attemptnumber', PARAM_INT);
7850 $mform->addElement('hidden', 'ajax', optional_param('ajax', 0, PARAM_INT));
7851 $mform->setType('ajax', PARAM_INT);
7852 $mform->addElement('hidden', 'userid', optional_param('userid', 0, PARAM_INT));
7853 $mform->setType('userid', PARAM_INT);
7855 if ($this->get_instance()->teamsubmission) {
7856 $mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign'));
7857 $mform->addElement('selectyesno', 'applytoall', get_string('applytoteam', 'assign'));
7858 $mform->setDefault('applytoall', 1);
7861 // Do not show if we are editing a previous attempt.
7862 if (($attemptnumber == -1 ||
7863 ($attemptnumber + 1) == count($this->get_all_submissions($userid))) &&
7864 $this->get_instance()->attemptreopenmethod != ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
7865 $mform->addElement('header', 'attemptsettings', get_string('attemptsettings', 'assign'));
7866 $attemptreopenmethod = get_string('attemptreopenmethod_' . $this->get_instance()->attemptreopenmethod, 'assign');
7867 $mform->addElement('static', 'attemptreopenmethod', get_string('attemptreopenmethod', 'assign'), $attemptreopenmethod);
7869 $attemptnumber = 0;
7870 if ($submission) {
7871 $attemptnumber = $submission->attemptnumber;
7873 $maxattempts = $this->get_instance()->maxattempts;
7874 if ($maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) {
7875 $maxattempts = get_string('unlimitedattempts', 'assign');
7877 $mform->addelement('static', 'maxattemptslabel', get_string('maxattempts', 'assign'), $maxattempts);
7878 $mform->addelement('static', 'attemptnumberlabel', get_string('attemptnumber', 'assign'), $attemptnumber + 1);
7880 $ismanual = $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL;
7881 $issubmission = !empty($submission);
7882 $isunlimited = $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS;
7883 $islessthanmaxattempts = $issubmission && ($submission->attemptnumber < ($this->get_instance()->maxattempts-1));
7885 if ($ismanual && (!$issubmission || $isunlimited || $islessthanmaxattempts)) {
7886 $mform->addElement('selectyesno', 'addattempt', get_string('addattempt', 'assign'));
7887 $mform->setDefault('addattempt', 0);
7890 if (!$gradingpanel) {
7891 $mform->addElement('selectyesno', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
7892 } else {
7893 $mform->addElement('hidden', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
7894 $mform->setType('sendstudentnotifications', PARAM_BOOL);
7896 // Get assignment visibility information for student.
7897 $modinfo = get_fast_modinfo($settings->course, $userid);
7898 $cm = $modinfo->get_cm($this->get_course_module()->id);
7900 // Don't allow notification to be sent if the student can't access the assignment,
7901 // or until in "Released" state if using marking workflow.
7902 if (!$cm->uservisible) {
7903 $mform->setDefault('sendstudentnotifications', 0);
7904 $mform->freeze('sendstudentnotifications');
7905 } else if ($this->get_instance()->markingworkflow) {
7906 $mform->setDefault('sendstudentnotifications', 0);
7907 if (!$gradingpanel) {
7908 $mform->disabledIf('sendstudentnotifications', 'workflowstate', 'neq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
7910 } else {
7911 $mform->setDefault('sendstudentnotifications', $this->get_instance()->sendstudentnotifications);
7914 $mform->addElement('hidden', 'action', 'submitgrade');
7915 $mform->setType('action', PARAM_ALPHA);
7917 if (!$gradingpanel) {
7919 $buttonarray = array();
7920 $name = get_string('savechanges', 'assign');
7921 $buttonarray[] = $mform->createElement('submit', 'savegrade', $name);
7922 if (!$last) {
7923 $name = get_string('savenext', 'assign');
7924 $buttonarray[] = $mform->createElement('submit', 'saveandshownext', $name);
7926 $buttonarray[] = $mform->createElement('cancel', 'cancelbutton', get_string('cancel'));
7927 $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
7928 $mform->closeHeaderBefore('buttonar');
7929 $buttonarray = array();
7931 if ($rownum > 0) {
7932 $name = get_string('previous', 'assign');
7933 $buttonarray[] = $mform->createElement('submit', 'nosaveandprevious', $name);
7936 if (!$last) {
7937 $name = get_string('nosavebutnext', 'assign');
7938 $buttonarray[] = $mform->createElement('submit', 'nosaveandnext', $name);
7940 if (!empty($buttonarray)) {
7941 $mform->addGroup($buttonarray, 'navar', '', array(' '), false);
7944 // The grading form does not work well with shortforms.
7945 $mform->setDisableShortforms();
7949 * Add elements in submission plugin form.
7951 * @param mixed $submission stdClass|null
7952 * @param MoodleQuickForm $mform
7953 * @param stdClass $data
7954 * @param int $userid The current userid (same as $USER->id)
7955 * @return void
7957 protected function add_plugin_submission_elements($submission,
7958 MoodleQuickForm $mform,
7959 stdClass $data,
7960 $userid) {
7961 foreach ($this->submissionplugins as $plugin) {
7962 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
7963 $plugin->get_form_elements_for_user($submission, $mform, $data, $userid);
7969 * Check if feedback plugins installed are enabled.
7971 * @return bool
7973 public function is_any_feedback_plugin_enabled() {
7974 if (!isset($this->cache['any_feedback_plugin_enabled'])) {
7975 $this->cache['any_feedback_plugin_enabled'] = false;
7976 foreach ($this->feedbackplugins as $plugin) {
7977 if ($plugin->is_enabled() && $plugin->is_visible()) {
7978 $this->cache['any_feedback_plugin_enabled'] = true;
7979 break;
7984 return $this->cache['any_feedback_plugin_enabled'];
7989 * Check if submission plugins installed are enabled.
7991 * @return bool
7993 public function is_any_submission_plugin_enabled() {
7994 if (!isset($this->cache['any_submission_plugin_enabled'])) {
7995 $this->cache['any_submission_plugin_enabled'] = false;
7996 foreach ($this->submissionplugins as $plugin) {
7997 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
7998 $this->cache['any_submission_plugin_enabled'] = true;
7999 break;
8004 return $this->cache['any_submission_plugin_enabled'];
8009 * Add elements to submission form.
8010 * @param MoodleQuickForm $mform
8011 * @param stdClass $data
8012 * @return void
8014 public function add_submission_form_elements(MoodleQuickForm $mform, stdClass $data) {
8015 global $USER;
8017 $userid = $data->userid;
8018 // Team submissions.
8019 if ($this->get_instance()->teamsubmission) {
8020 $submission = $this->get_group_submission($userid, 0, false);
8021 } else {
8022 $submission = $this->get_user_submission($userid, false);
8025 // Submission statement.
8026 $adminconfig = $this->get_admin_config();
8027 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
8029 $draftsenabled = $this->get_instance()->submissiondrafts;
8030 $submissionstatement = '';
8032 if ($requiresubmissionstatement) {
8033 $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
8036 // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
8037 // that the submission statement checkbox will be displayed.
8038 if (empty($submissionstatement)) {
8039 $requiresubmissionstatement = false;
8042 // Only show submission statement if we are editing our own submission.
8043 if ($requiresubmissionstatement && !$draftsenabled && $userid == $USER->id) {
8044 $mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement);
8045 $mform->addRule('submissionstatement', get_string('required'), 'required', null, 'client');
8048 $this->add_plugin_submission_elements($submission, $mform, $data, $userid);
8050 // Hidden params.
8051 $mform->addElement('hidden', 'id', $this->get_course_module()->id);
8052 $mform->setType('id', PARAM_INT);
8054 $mform->addElement('hidden', 'userid', $userid);
8055 $mform->setType('userid', PARAM_INT);
8057 $mform->addElement('hidden', 'action', 'savesubmission');
8058 $mform->setType('action', PARAM_ALPHA);
8062 * Remove any data from the current submission.
8064 * @param int $userid
8065 * @return boolean
8067 public function remove_submission($userid) {
8068 global $USER;
8070 if (!$this->can_edit_submission($userid, $USER->id)) {
8071 $user = core_user::get_user($userid);
8072 $message = get_string('usersubmissioncannotberemoved', 'assign', fullname($user));
8073 $this->set_error_message($message);
8074 return false;
8077 if ($this->get_instance()->teamsubmission) {
8078 $submission = $this->get_group_submission($userid, 0, false);
8079 } else {
8080 $submission = $this->get_user_submission($userid, false);
8083 if (!$submission) {
8084 return false;
8086 $submission->status = $submission->attemptnumber ? ASSIGN_SUBMISSION_STATUS_REOPENED : ASSIGN_SUBMISSION_STATUS_NEW;
8087 $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
8089 // Tell each submission plugin we were saved with no data.
8090 $plugins = $this->get_submission_plugins();
8091 foreach ($plugins as $plugin) {
8092 if ($plugin->is_enabled() && $plugin->is_visible()) {
8093 $plugin->remove($submission);
8097 $completion = new completion_info($this->get_course());
8098 if ($completion->is_enabled($this->get_course_module()) &&
8099 $this->get_instance()->completionsubmit) {
8100 $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
8103 if ($submission->userid != 0) {
8104 \mod_assign\event\submission_status_updated::create_from_submission($this, $submission)->trigger();
8106 return true;
8110 * Revert to draft.
8112 * @param int $userid
8113 * @return boolean
8115 public function revert_to_draft($userid) {
8116 global $DB, $USER;
8118 // Need grade permission.
8119 require_capability('mod/assign:grade', $this->context);
8121 if ($this->get_instance()->teamsubmission) {
8122 $submission = $this->get_group_submission($userid, 0, false);
8123 } else {
8124 $submission = $this->get_user_submission($userid, false);
8127 if (!$submission) {
8128 return false;
8130 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
8131 $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
8133 // Give each submission plugin a chance to process the reverting to draft.
8134 $plugins = $this->get_submission_plugins();
8135 foreach ($plugins as $plugin) {
8136 if ($plugin->is_enabled() && $plugin->is_visible()) {
8137 $plugin->revert_to_draft($submission);
8140 // Update the modified time on the grade (grader modified).
8141 $grade = $this->get_user_grade($userid, true);
8142 $grade->grader = $USER->id;
8143 $this->update_grade($grade);
8145 $completion = new completion_info($this->get_course());
8146 if ($completion->is_enabled($this->get_course_module()) &&
8147 $this->get_instance()->completionsubmit) {
8148 $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
8150 \mod_assign\event\submission_status_updated::create_from_submission($this, $submission)->trigger();
8151 return true;
8155 * Remove the current submission.
8157 * @param int $userid
8158 * @return boolean
8160 protected function process_remove_submission($userid = 0) {
8161 require_sesskey();
8163 if (!$userid) {
8164 $userid = required_param('userid', PARAM_INT);
8167 return $this->remove_submission($userid);
8171 * Revert to draft.
8172 * Uses url parameter userid if userid not supplied as a parameter.
8174 * @param int $userid
8175 * @return boolean
8177 protected function process_revert_to_draft($userid = 0) {
8178 require_sesskey();
8180 if (!$userid) {
8181 $userid = required_param('userid', PARAM_INT);
8184 return $this->revert_to_draft($userid);
8188 * Prevent student updates to this submission
8190 * @param int $userid
8191 * @return bool
8193 public function lock_submission($userid) {
8194 global $USER, $DB;
8195 // Need grade permission.
8196 require_capability('mod/assign:grade', $this->context);
8198 // Give each submission plugin a chance to process the locking.
8199 $plugins = $this->get_submission_plugins();
8200 $submission = $this->get_user_submission($userid, false);
8202 $flags = $this->get_user_flags($userid, true);
8203 $flags->locked = 1;
8204 $this->update_user_flags($flags);
8206 foreach ($plugins as $plugin) {
8207 if ($plugin->is_enabled() && $plugin->is_visible()) {
8208 $plugin->lock($submission, $flags);
8212 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8213 \mod_assign\event\submission_locked::create_from_user($this, $user)->trigger();
8214 return true;
8219 * Set the workflow state for multiple users
8221 * @return void
8223 protected function process_set_batch_marking_workflow_state() {
8224 global $CFG, $DB;
8226 // Include batch marking workflow form.
8227 require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
8229 $formparams = array(
8230 'userscount' => 0, // This form is never re-displayed, so we don't need to
8231 'usershtml' => '', // initialise these parameters with real information.
8232 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
8235 $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
8237 if ($mform->is_cancelled()) {
8238 return true;
8241 if ($formdata = $mform->get_data()) {
8242 $useridlist = explode(',', $formdata->selectedusers);
8243 $state = $formdata->markingworkflowstate;
8245 foreach ($useridlist as $userid) {
8246 $flags = $this->get_user_flags($userid, true);
8248 $flags->workflowstate = $state;
8250 // Clear the mailed flag if notification is requested, the student hasn't been
8251 // notified previously, the student can access the assignment, and the state
8252 // is "Released".
8253 $modinfo = get_fast_modinfo($this->course, $userid);
8254 $cm = $modinfo->get_cm($this->get_course_module()->id);
8255 if ($formdata->sendstudentnotifications && $cm->uservisible &&
8256 $state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8257 $flags->mailed = 0;
8260 $gradingdisabled = $this->grading_disabled($userid);
8262 // Will not apply update if user does not have permission to assign this workflow state.
8263 if (!$gradingdisabled && $this->update_user_flags($flags)) {
8264 // Update Gradebook.
8265 $grade = $this->get_user_grade($userid, true);
8266 // Fetch any feedback for this student.
8267 $gradebookplugin = $this->get_admin_config()->feedback_plugin_for_gradebook;
8268 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
8269 $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
8270 if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
8271 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
8272 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
8273 $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
8275 $this->update_grade($grade);
8276 $assign = clone $this->get_instance();
8277 $assign->cmidnumber = $this->get_course_module()->idnumber;
8278 // Set assign gradebook feedback plugin status.
8279 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
8281 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8282 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $state)->trigger();
8289 * Set the marking allocation for multiple users
8291 * @return void
8293 protected function process_set_batch_marking_allocation() {
8294 global $CFG, $DB;
8296 // Include batch marking allocation form.
8297 require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
8299 $formparams = array(
8300 'userscount' => 0, // This form is never re-displayed, so we don't need to
8301 'usershtml' => '' // initialise these parameters with real information.
8304 list($sort, $params) = users_order_by_sql('u');
8305 // Only enrolled users could be assigned as potential markers.
8306 $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
8307 $markerlist = array();
8308 foreach ($markers as $marker) {
8309 $markerlist[$marker->id] = fullname($marker);
8312 $formparams['markers'] = $markerlist;
8314 $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
8316 if ($mform->is_cancelled()) {
8317 return true;
8320 if ($formdata = $mform->get_data()) {
8321 $useridlist = explode(',', $formdata->selectedusers);
8322 $marker = $DB->get_record('user', array('id' => $formdata->allocatedmarker), '*', MUST_EXIST);
8324 foreach ($useridlist as $userid) {
8325 $flags = $this->get_user_flags($userid, true);
8326 if ($flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW ||
8327 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW ||
8328 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE ||
8329 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8331 continue; // Allocated marker can only be changed in certain workflow states.
8334 $flags->allocatedmarker = $marker->id;
8336 if ($this->update_user_flags($flags)) {
8337 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8338 \mod_assign\event\marker_updated::create_from_marker($this, $user, $marker)->trigger();
8346 * Prevent student updates to this submission.
8347 * Uses url parameter userid.
8349 * @param int $userid
8350 * @return void
8352 protected function process_lock_submission($userid = 0) {
8354 require_sesskey();
8356 if (!$userid) {
8357 $userid = required_param('userid', PARAM_INT);
8360 return $this->lock_submission($userid);
8364 * Unlock the student submission.
8366 * @param int $userid
8367 * @return bool
8369 public function unlock_submission($userid) {
8370 global $USER, $DB;
8372 // Need grade permission.
8373 require_capability('mod/assign:grade', $this->context);
8375 // Give each submission plugin a chance to process the unlocking.
8376 $plugins = $this->get_submission_plugins();
8377 $submission = $this->get_user_submission($userid, false);
8379 $flags = $this->get_user_flags($userid, true);
8380 $flags->locked = 0;
8381 $this->update_user_flags($flags);
8383 foreach ($plugins as $plugin) {
8384 if ($plugin->is_enabled() && $plugin->is_visible()) {
8385 $plugin->unlock($submission, $flags);
8389 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8390 \mod_assign\event\submission_unlocked::create_from_user($this, $user)->trigger();
8391 return true;
8395 * Unlock the student submission.
8396 * Uses url parameter userid.
8398 * @param int $userid
8399 * @return bool
8401 protected function process_unlock_submission($userid = 0) {
8403 require_sesskey();
8405 if (!$userid) {
8406 $userid = required_param('userid', PARAM_INT);
8409 return $this->unlock_submission($userid);
8413 * Apply a grade from a grading form to a user (may be called multiple times for a group submission).
8415 * @param stdClass $formdata - the data from the form
8416 * @param int $userid - the user to apply the grade to
8417 * @param int $attemptnumber - The attempt number to apply the grade to.
8418 * @return void
8420 protected function apply_grade_to_user($formdata, $userid, $attemptnumber) {
8421 global $USER, $CFG, $DB;
8423 $grade = $this->get_user_grade($userid, true, $attemptnumber);
8424 $originalgrade = $grade->grade;
8425 $gradingdisabled = $this->grading_disabled($userid);
8426 $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
8427 if (!$gradingdisabled) {
8428 if ($gradinginstance) {
8429 $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading,
8430 $grade->id);
8431 } else {
8432 // Handle the case when grade is set to No Grade.
8433 if (isset($formdata->grade)) {
8434 $grade->grade = grade_floatval(unformat_float($formdata->grade));
8437 if (isset($formdata->workflowstate) || isset($formdata->allocatedmarker)) {
8438 $flags = $this->get_user_flags($userid, true);
8439 $oldworkflowstate = $flags->workflowstate;
8440 $flags->workflowstate = isset($formdata->workflowstate) ? $formdata->workflowstate : $flags->workflowstate;
8441 $flags->allocatedmarker = isset($formdata->allocatedmarker) ? $formdata->allocatedmarker : $flags->allocatedmarker;
8442 if ($this->update_user_flags($flags) &&
8443 isset($formdata->workflowstate) &&
8444 $formdata->workflowstate !== $oldworkflowstate) {
8445 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8446 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $formdata->workflowstate)->trigger();
8450 $grade->grader= $USER->id;
8452 $adminconfig = $this->get_admin_config();
8453 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
8455 $feedbackmodified = false;
8457 // Call save in plugins.
8458 foreach ($this->feedbackplugins as $plugin) {
8459 if ($plugin->is_enabled() && $plugin->is_visible()) {
8460 $gradingmodified = $plugin->is_feedback_modified($grade, $formdata);
8461 if ($gradingmodified) {
8462 if (!$plugin->save($grade, $formdata)) {
8463 $result = false;
8464 print_error($plugin->get_error());
8466 // If $feedbackmodified is true, keep it true.
8467 $feedbackmodified = $feedbackmodified || $gradingmodified;
8469 if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
8470 // This is the feedback plugin chose to push comments to the gradebook.
8471 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
8472 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
8473 $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
8478 // We do not want to update the timemodified if no grade was added.
8479 if (!empty($formdata->addattempt) ||
8480 ($originalgrade !== null && $originalgrade != -1) ||
8481 ($grade->grade !== null && $grade->grade != -1) ||
8482 $feedbackmodified) {
8483 $this->update_grade($grade, !empty($formdata->addattempt));
8486 // We never send notifications if we have marking workflow and the grade is not released.
8487 if ($this->get_instance()->markingworkflow &&
8488 isset($formdata->workflowstate) &&
8489 $formdata->workflowstate != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8490 $formdata->sendstudentnotifications = false;
8493 // Note the default if not provided for this option is true (e.g. webservices).
8494 // This is for backwards compatibility.
8495 if (!isset($formdata->sendstudentnotifications) || $formdata->sendstudentnotifications) {
8496 $this->notify_grade_modified($grade, true);
8502 * Save outcomes submitted from grading form.
8504 * @param int $userid
8505 * @param stdClass $formdata
8506 * @param int $sourceuserid The user ID under which the outcome data is accessible. This is relevant
8507 * for an outcome set to a user but applied to an entire group.
8509 protected function process_outcomes($userid, $formdata, $sourceuserid = null) {
8510 global $CFG, $USER;
8512 if (empty($CFG->enableoutcomes)) {
8513 return;
8515 if ($this->grading_disabled($userid)) {
8516 return;
8519 require_once($CFG->libdir.'/gradelib.php');
8521 $data = array();
8522 $gradinginfo = grade_get_grades($this->get_course()->id,
8523 'mod',
8524 'assign',
8525 $this->get_instance()->id,
8526 $userid);
8528 if (!empty($gradinginfo->outcomes)) {
8529 foreach ($gradinginfo->outcomes as $index => $oldoutcome) {
8530 $name = 'outcome_'.$index;
8531 $sourceuserid = $sourceuserid !== null ? $sourceuserid : $userid;
8532 if (isset($formdata->{$name}[$sourceuserid]) &&
8533 $oldoutcome->grades[$userid]->grade != $formdata->{$name}[$sourceuserid]) {
8534 $data[$index] = $formdata->{$name}[$sourceuserid];
8538 if (count($data) > 0) {
8539 grade_update_outcomes('mod/assign',
8540 $this->course->id,
8541 'mod',
8542 'assign',
8543 $this->get_instance()->id,
8544 $userid,
8545 $data);
8550 * If the requirements are met - reopen the submission for another attempt.
8551 * Only call this function when grading the latest attempt.
8553 * @param int $userid The userid.
8554 * @param stdClass $submission The submission (may be a group submission).
8555 * @param bool $addattempt - True if the "allow another attempt" checkbox was checked.
8556 * @return bool - true if another attempt was added.
8558 protected function reopen_submission_if_required($userid, $submission, $addattempt) {
8559 $instance = $this->get_instance();
8560 $maxattemptsreached = !empty($submission) &&
8561 $submission->attemptnumber >= ($instance->maxattempts - 1) &&
8562 $instance->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS;
8563 $shouldreopen = false;
8564 if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS) {
8565 // Check the gradetopass from the gradebook.
8566 $gradeitem = $this->get_grade_item();
8567 if ($gradeitem) {
8568 $gradegrade = grade_grade::fetch(array('userid' => $userid, 'itemid' => $gradeitem->id));
8570 // Do not reopen if is_passed returns null, e.g. if there is no pass criterion set.
8571 if ($gradegrade && ($gradegrade->is_passed() === false)) {
8572 $shouldreopen = true;
8576 if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL &&
8577 !empty($addattempt)) {
8578 $shouldreopen = true;
8580 if ($shouldreopen && !$maxattemptsreached) {
8581 $this->add_attempt($userid);
8582 return true;
8584 return false;
8588 * Save grade update.
8590 * @param int $userid
8591 * @param stdClass $data
8592 * @return bool - was the grade saved
8594 public function save_grade($userid, $data) {
8596 // Need grade permission.
8597 require_capability('mod/assign:grade', $this->context);
8599 $instance = $this->get_instance();
8600 $submission = null;
8601 if ($instance->teamsubmission) {
8602 // We need to know what the most recent group submission is.
8603 // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
8604 // and when deciding if we need to update the gradebook with an edited grade.
8605 $mostrecentsubmission = $this->get_group_submission($userid, 0, false, -1);
8606 $this->set_most_recent_team_submission($mostrecentsubmission);
8607 // Get the submission that we are saving grades for. The data attempt number determines which submission attempt.
8608 $submission = $this->get_group_submission($userid, 0, false, $data->attemptnumber);
8609 } else {
8610 $submission = $this->get_user_submission($userid, false, $data->attemptnumber);
8612 if ($instance->teamsubmission && !empty($data->applytoall)) {
8613 $groupid = 0;
8614 if ($this->get_submission_group($userid)) {
8615 $group = $this->get_submission_group($userid);
8616 if ($group) {
8617 $groupid = $group->id;
8620 $members = $this->get_submission_group_members($groupid, true, $this->show_only_active_users());
8621 foreach ($members as $member) {
8622 // We only want to update the grade for this group submission attempt. The data attempt number could be
8623 // -1 which may end up in additional attempts being created for each group member instead of just one
8624 // additional attempt for the group.
8625 $this->apply_grade_to_user($data, $member->id, $submission->attemptnumber);
8626 $this->process_outcomes($member->id, $data, $userid);
8628 } else {
8629 $this->apply_grade_to_user($data, $userid, $data->attemptnumber);
8631 $this->process_outcomes($userid, $data);
8634 return true;
8638 * Save grade.
8640 * @param moodleform $mform
8641 * @return bool - was the grade saved
8643 protected function process_save_grade(&$mform) {
8644 global $CFG, $SESSION;
8645 // Include grade form.
8646 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
8648 require_sesskey();
8650 $instance = $this->get_instance();
8651 $rownum = required_param('rownum', PARAM_INT);
8652 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
8653 $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
8654 $userid = optional_param('userid', 0, PARAM_INT);
8655 if (!$userid) {
8656 if (empty($SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)])) {
8657 // If the userid list is not stored we must not save, as it is possible that the user in a
8658 // given row position may not be the same now as when the grading page was generated.
8659 $url = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
8660 throw new moodle_exception('useridlistnotcached', 'mod_assign', $url);
8662 $useridlist = $SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)];
8663 } else {
8664 $useridlist = array($userid);
8665 $rownum = 0;
8668 $last = false;
8669 $userid = $useridlist[$rownum];
8670 if ($rownum == count($useridlist) - 1) {
8671 $last = true;
8674 $data = new stdClass();
8676 $gradeformparams = array('rownum' => $rownum,
8677 'useridlistid' => $useridlistid,
8678 'last' => $last,
8679 'attemptnumber' => $attemptnumber,
8680 'userid' => $userid);
8681 $mform = new mod_assign_grade_form(null,
8682 array($this, $data, $gradeformparams),
8683 'post',
8685 array('class'=>'gradeform'));
8687 if ($formdata = $mform->get_data()) {
8688 return $this->save_grade($userid, $formdata);
8689 } else {
8690 return false;
8695 * This function is a static wrapper around can_upgrade.
8697 * @param string $type The plugin type
8698 * @param int $version The plugin version
8699 * @return bool
8701 public static function can_upgrade_assignment($type, $version) {
8702 $assignment = new assign(null, null, null);
8703 return $assignment->can_upgrade($type, $version);
8707 * This function returns true if it can upgrade an assignment from the 2.2 module.
8709 * @param string $type The plugin type
8710 * @param int $version The plugin version
8711 * @return bool
8713 public function can_upgrade($type, $version) {
8714 if ($type == 'offline' && $version >= 2011112900) {
8715 return true;
8717 foreach ($this->submissionplugins as $plugin) {
8718 if ($plugin->can_upgrade($type, $version)) {
8719 return true;
8722 foreach ($this->feedbackplugins as $plugin) {
8723 if ($plugin->can_upgrade($type, $version)) {
8724 return true;
8727 return false;
8731 * Copy all the files from the old assignment files area to the new one.
8732 * This is used by the plugin upgrade code.
8734 * @param int $oldcontextid The old assignment context id
8735 * @param int $oldcomponent The old assignment component ('assignment')
8736 * @param int $oldfilearea The old assignment filearea ('submissions')
8737 * @param int $olditemid The old submissionid (can be null e.g. intro)
8738 * @param int $newcontextid The new assignment context id
8739 * @param int $newcomponent The new assignment component ('assignment')
8740 * @param int $newfilearea The new assignment filearea ('submissions')
8741 * @param int $newitemid The new submissionid (can be null e.g. intro)
8742 * @return int The number of files copied
8744 public function copy_area_files_for_upgrade($oldcontextid,
8745 $oldcomponent,
8746 $oldfilearea,
8747 $olditemid,
8748 $newcontextid,
8749 $newcomponent,
8750 $newfilearea,
8751 $newitemid) {
8752 // Note, this code is based on some code in filestorage - but that code
8753 // deleted the old files (which we don't want).
8754 $count = 0;
8756 $fs = get_file_storage();
8758 $oldfiles = $fs->get_area_files($oldcontextid,
8759 $oldcomponent,
8760 $oldfilearea,
8761 $olditemid,
8762 'id',
8763 false);
8764 foreach ($oldfiles as $oldfile) {
8765 $filerecord = new stdClass();
8766 $filerecord->contextid = $newcontextid;
8767 $filerecord->component = $newcomponent;
8768 $filerecord->filearea = $newfilearea;
8769 $filerecord->itemid = $newitemid;
8770 $fs->create_file_from_storedfile($filerecord, $oldfile);
8771 $count += 1;
8774 return $count;
8778 * Add a new attempt for each user in the list - but reopen each group assignment
8779 * at most 1 time.
8781 * @param array $useridlist Array of userids to reopen.
8782 * @return bool
8784 protected function process_add_attempt_group($useridlist) {
8785 $groupsprocessed = array();
8786 $result = true;
8788 foreach ($useridlist as $userid) {
8789 $groupid = 0;
8790 $group = $this->get_submission_group($userid);
8791 if ($group) {
8792 $groupid = $group->id;
8795 if (empty($groupsprocessed[$groupid])) {
8796 // We need to know what the most recent group submission is.
8797 // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
8798 // and when deciding if we need to update the gradebook with an edited grade.
8799 $currentsubmission = $this->get_group_submission($userid, 0, false, -1);
8800 $this->set_most_recent_team_submission($currentsubmission);
8801 $result = $this->process_add_attempt($userid) && $result;
8802 $groupsprocessed[$groupid] = true;
8805 return $result;
8809 * Check for a sess key and then call add_attempt.
8811 * @param int $userid int The user to add the attempt for
8812 * @return bool - true if successful.
8814 protected function process_add_attempt($userid) {
8815 require_sesskey();
8817 return $this->add_attempt($userid);
8821 * Add a new attempt for a user.
8823 * @param int $userid int The user to add the attempt for
8824 * @return bool - true if successful.
8826 protected function add_attempt($userid) {
8827 require_capability('mod/assign:grade', $this->context);
8829 if ($this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
8830 return false;
8833 if ($this->get_instance()->teamsubmission) {
8834 $oldsubmission = $this->get_group_submission($userid, 0, false);
8835 } else {
8836 $oldsubmission = $this->get_user_submission($userid, false);
8839 if (!$oldsubmission) {
8840 return false;
8843 // No more than max attempts allowed.
8844 if ($this->get_instance()->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS &&
8845 $oldsubmission->attemptnumber >= ($this->get_instance()->maxattempts - 1)) {
8846 return false;
8849 // Create the new submission record for the group/user.
8850 if ($this->get_instance()->teamsubmission) {
8851 if (isset($this->mostrecentteamsubmission)) {
8852 // Team submissions can end up in this function for each user (via save_grade). We don't want to create
8853 // more than one attempt for the whole team.
8854 if ($this->mostrecentteamsubmission->attemptnumber == $oldsubmission->attemptnumber) {
8855 $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
8856 } else {
8857 $newsubmission = $this->get_group_submission($userid, 0, false, $oldsubmission->attemptnumber);
8859 } else {
8860 debugging('Please use set_most_recent_team_submission() before calling add_attempt', DEBUG_DEVELOPER);
8861 $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
8863 } else {
8864 $newsubmission = $this->get_user_submission($userid, true, $oldsubmission->attemptnumber + 1);
8867 // Set the status of the new attempt to reopened.
8868 $newsubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
8870 // Give each submission plugin a chance to process the add_attempt.
8871 $plugins = $this->get_submission_plugins();
8872 foreach ($plugins as $plugin) {
8873 if ($plugin->is_enabled() && $plugin->is_visible()) {
8874 $plugin->add_attempt($oldsubmission, $newsubmission);
8878 $this->update_submission($newsubmission, $userid, false, $this->get_instance()->teamsubmission);
8879 $flags = $this->get_user_flags($userid, false);
8880 if (isset($flags->locked) && $flags->locked) { // May not exist.
8881 $this->process_unlock_submission($userid);
8883 return true;
8887 * Get an upto date list of user grades and feedback for the gradebook.
8889 * @param int $userid int or 0 for all users
8890 * @return array of grade data formated for the gradebook api
8891 * The data required by the gradebook api is userid,
8892 * rawgrade,
8893 * feedback,
8894 * feedbackformat,
8895 * usermodified,
8896 * dategraded,
8897 * datesubmitted
8899 public function get_user_grades_for_gradebook($userid) {
8900 global $DB, $CFG;
8901 $grades = array();
8902 $assignmentid = $this->get_instance()->id;
8904 $adminconfig = $this->get_admin_config();
8905 $gradebookpluginname = $adminconfig->feedback_plugin_for_gradebook;
8906 $gradebookplugin = null;
8908 // Find the gradebook plugin.
8909 foreach ($this->feedbackplugins as $plugin) {
8910 if ($plugin->is_enabled() && $plugin->is_visible()) {
8911 if (('assignfeedback_' . $plugin->get_type()) == $gradebookpluginname) {
8912 $gradebookplugin = $plugin;
8916 if ($userid) {
8917 $where = ' WHERE u.id = :userid ';
8918 } else {
8919 $where = ' WHERE u.id != :userid ';
8922 // When the gradebook asks us for grades - only return the last attempt for each user.
8923 $params = array('assignid1'=>$assignmentid,
8924 'assignid2'=>$assignmentid,
8925 'userid'=>$userid);
8926 $graderesults = $DB->get_recordset_sql('SELECT
8927 u.id as userid,
8928 s.timemodified as datesubmitted,
8929 g.grade as rawgrade,
8930 g.timemodified as dategraded,
8931 g.grader as usermodified
8932 FROM {user} u
8933 LEFT JOIN {assign_submission} s
8934 ON u.id = s.userid and s.assignment = :assignid1 AND
8935 s.latest = 1
8936 JOIN {assign_grades} g
8937 ON u.id = g.userid and g.assignment = :assignid2 AND
8938 g.attemptnumber = s.attemptnumber' .
8939 $where, $params);
8941 foreach ($graderesults as $result) {
8942 $gradingstatus = $this->get_grading_status($result->userid);
8943 if (!$this->get_instance()->markingworkflow || $gradingstatus == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8944 $gradebookgrade = clone $result;
8945 // Now get the feedback.
8946 if ($gradebookplugin) {
8947 $grade = $this->get_user_grade($result->userid, false);
8948 if ($grade) {
8949 $gradebookgrade->feedback = $gradebookplugin->text_for_gradebook($grade);
8950 $gradebookgrade->feedbackformat = $gradebookplugin->format_for_gradebook($grade);
8951 $gradebookgrade->feedbackfiles = $gradebookplugin->files_for_gradebook($grade);
8954 $grades[$gradebookgrade->userid] = $gradebookgrade;
8958 $graderesults->close();
8959 return $grades;
8963 * Call the static version of this function
8965 * @param int $userid The userid to lookup
8966 * @return int The unique id
8968 public function get_uniqueid_for_user($userid) {
8969 return self::get_uniqueid_for_user_static($this->get_instance()->id, $userid);
8973 * Foreach participant in the course - assign them a random id.
8975 * @param int $assignid The assignid to lookup
8977 public static function allocate_unique_ids($assignid) {
8978 global $DB;
8980 $cm = get_coursemodule_from_instance('assign', $assignid, 0, false, MUST_EXIST);
8981 $context = context_module::instance($cm->id);
8983 $currentgroup = groups_get_activity_group($cm, true);
8984 $users = get_enrolled_users($context, "mod/assign:submit", $currentgroup, 'u.id');
8986 // Shuffle the users.
8987 shuffle($users);
8989 foreach ($users as $user) {
8990 $record = $DB->get_record('assign_user_mapping',
8991 array('assignment'=>$assignid, 'userid'=>$user->id),
8992 'id');
8993 if (!$record) {
8994 $record = new stdClass();
8995 $record->assignment = $assignid;
8996 $record->userid = $user->id;
8997 $DB->insert_record('assign_user_mapping', $record);
9003 * Lookup this user id and return the unique id for this assignment.
9005 * @param int $assignid The assignment id
9006 * @param int $userid The userid to lookup
9007 * @return int The unique id
9009 public static function get_uniqueid_for_user_static($assignid, $userid) {
9010 global $DB;
9012 // Search for a record.
9013 $params = array('assignment'=>$assignid, 'userid'=>$userid);
9014 if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
9015 return $record->id;
9018 // Be a little smart about this - there is no record for the current user.
9019 // We should ensure any unallocated ids for the current participant
9020 // list are distrubited randomly.
9021 self::allocate_unique_ids($assignid);
9023 // Retry the search for a record.
9024 if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
9025 return $record->id;
9028 // The requested user must not be a participant. Add a record anyway.
9029 $record = new stdClass();
9030 $record->assignment = $assignid;
9031 $record->userid = $userid;
9033 return $DB->insert_record('assign_user_mapping', $record);
9037 * Call the static version of this function.
9039 * @param int $uniqueid The uniqueid to lookup
9040 * @return int The user id or false if they don't exist
9042 public function get_user_id_for_uniqueid($uniqueid) {
9043 return self::get_user_id_for_uniqueid_static($this->get_instance()->id, $uniqueid);
9047 * Lookup this unique id and return the user id for this assignment.
9049 * @param int $assignid The id of the assignment this user mapping is in
9050 * @param int $uniqueid The uniqueid to lookup
9051 * @return int The user id or false if they don't exist
9053 public static function get_user_id_for_uniqueid_static($assignid, $uniqueid) {
9054 global $DB;
9056 // Search for a record.
9057 if ($record = $DB->get_record('assign_user_mapping',
9058 array('assignment'=>$assignid, 'id'=>$uniqueid),
9059 'userid',
9060 IGNORE_MISSING)) {
9061 return $record->userid;
9064 return false;
9068 * Get the list of marking_workflow states the current user has permission to transition a grade to.
9070 * @return array of state => description
9072 public function get_marking_workflow_states_for_current_user() {
9073 if (!empty($this->markingworkflowstates)) {
9074 return $this->markingworkflowstates;
9076 $states = array();
9077 if (has_capability('mod/assign:grade', $this->context)) {
9078 $states[ASSIGN_MARKING_WORKFLOW_STATE_INMARKING] = get_string('markingworkflowstateinmarking', 'assign');
9079 $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW] = get_string('markingworkflowstatereadyforreview', 'assign');
9081 if (has_any_capability(array('mod/assign:reviewgrades',
9082 'mod/assign:managegrades'), $this->context)) {
9083 $states[ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW] = get_string('markingworkflowstateinreview', 'assign');
9084 $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE] = get_string('markingworkflowstatereadyforrelease', 'assign');
9086 if (has_any_capability(array('mod/assign:releasegrades',
9087 'mod/assign:managegrades'), $this->context)) {
9088 $states[ASSIGN_MARKING_WORKFLOW_STATE_RELEASED] = get_string('markingworkflowstatereleased', 'assign');
9090 $this->markingworkflowstates = $states;
9091 return $this->markingworkflowstates;
9095 * Check is only active users in course should be shown.
9097 * @return bool true if only active users should be shown.
9099 public function show_only_active_users() {
9100 global $CFG;
9102 if (is_null($this->showonlyactiveenrol)) {
9103 $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
9104 $this->showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
9106 if (!is_null($this->context)) {
9107 $this->showonlyactiveenrol = $this->showonlyactiveenrol ||
9108 !has_capability('moodle/course:viewsuspendedusers', $this->context);
9111 return $this->showonlyactiveenrol;
9115 * Return true is user is active user in course else false
9117 * @param int $userid
9118 * @return bool true is user is active in course.
9120 public function is_active_user($userid) {
9121 return !in_array($userid, get_suspended_userids($this->context, true));
9125 * Returns true if gradebook feedback plugin is enabled
9127 * @return bool true if gradebook feedback plugin is enabled and visible else false.
9129 public function is_gradebook_feedback_enabled() {
9130 // Get default grade book feedback plugin.
9131 $adminconfig = $this->get_admin_config();
9132 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
9133 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
9135 // Check if default gradebook feedback is visible and enabled.
9136 $gradebookfeedbackplugin = $this->get_feedback_plugin_by_type($gradebookplugin);
9138 if (empty($gradebookfeedbackplugin)) {
9139 return false;
9142 if ($gradebookfeedbackplugin->is_visible() && $gradebookfeedbackplugin->is_enabled()) {
9143 return true;
9146 // Gradebook feedback plugin is either not visible/enabled.
9147 return false;
9151 * Returns the grading status.
9153 * @param int $userid the user id
9154 * @return string returns the grading status
9156 public function get_grading_status($userid) {
9157 if ($this->get_instance()->markingworkflow) {
9158 $flags = $this->get_user_flags($userid, false);
9159 if (!empty($flags->workflowstate)) {
9160 return $flags->workflowstate;
9162 return ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED;
9163 } else {
9164 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
9165 $grade = $this->get_user_grade($userid, false, $attemptnumber);
9167 if (!empty($grade) && $grade->grade !== null && $grade->grade >= 0) {
9168 return ASSIGN_GRADING_STATUS_GRADED;
9169 } else {
9170 return ASSIGN_GRADING_STATUS_NOT_GRADED;
9176 * The id used to uniquily identify the cache for this instance of the assign object.
9178 * @return string
9180 public function get_useridlist_key_id() {
9181 return $this->useridlistid;
9185 * Generates the key that should be used for an entry in the useridlist cache.
9187 * @param string $id Generate a key for this instance (optional)
9188 * @return string The key for the id, or new entry if no $id is passed.
9190 public function get_useridlist_key($id = null) {
9191 if ($id === null) {
9192 $id = $this->get_useridlist_key_id();
9194 return $this->get_course_module()->id . '_' . $id;
9198 * Updates and creates the completion records in mdl_course_modules_completion.
9200 * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity
9201 * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit
9202 * @param obj $submission the submission
9203 * @param int $userid the user id
9204 * @param int $complete
9205 * @param obj $completion
9207 * @return null
9209 protected function update_activity_completion_records($teamsubmission,
9210 $requireallteammemberssubmit,
9211 $submission,
9212 $userid,
9213 $complete,
9214 $completion) {
9216 if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) ||
9217 ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit &&
9218 $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
9220 $members = groups_get_members($submission->groupid);
9222 foreach ($members as $member) {
9223 $completion->update_state($this->get_course_module(), $complete, $member->id);
9225 } else {
9226 $completion->update_state($this->get_course_module(), $complete, $userid);
9229 return;
9233 * Update the module completion status (set it viewed) and trigger module viewed event.
9235 * @since Moodle 3.2
9237 public function set_module_viewed() {
9238 $completion = new completion_info($this->get_course());
9239 $completion->set_module_viewed($this->get_course_module());
9241 // Trigger the course module viewed event.
9242 $assigninstance = $this->get_instance();
9243 $params = [
9244 'objectid' => $assigninstance->id,
9245 'context' => $this->get_context()
9247 if ($this->is_blind_marking()) {
9248 $params['anonymous'] = 1;
9251 $event = \mod_assign\event\course_module_viewed::create($params);
9253 $event->add_record_snapshot('assign', $assigninstance);
9254 $event->trigger();
9258 * Checks for any grade notices, and adds notifications. Will display on assignment main page and grading table.
9260 * @return void The notifications API will render the notifications at the appropriate part of the page.
9262 protected function add_grade_notices() {
9263 if (has_capability('mod/assign:grade', $this->get_context()) && get_config('assign', 'has_rescaled_null_grades_' . $this->get_instance()->id)) {
9264 $link = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades'));
9265 \core\notification::warning(get_string('fixrescalednullgrades', 'mod_assign', ['link' => $link->out()]));
9270 * View fix rescaled null grades.
9272 * @return bool True if null all grades are now fixed.
9274 protected function fix_null_grades() {
9275 global $DB;
9276 $result = $DB->set_field_select(
9277 'assign_grades',
9278 'grade',
9279 ASSIGN_GRADE_NOT_SET,
9280 'grade <> ? AND grade < 0',
9281 [ASSIGN_GRADE_NOT_SET]
9283 $assign = clone $this->get_instance();
9284 $assign->cmidnumber = $this->get_course_module()->idnumber;
9285 assign_update_grades($assign);
9286 return $result;
9290 * View fix rescaled null grades.
9292 * @return void The notifications API will render the notifications at the appropriate part of the page.
9294 protected function view_fix_rescaled_null_grades() {
9295 global $OUTPUT;
9297 $o = '';
9299 require_capability('mod/assign:grade', $this->get_context());
9301 $instance = $this->get_instance();
9303 $o .= $this->get_renderer()->render(
9304 new assign_header(
9305 $instance,
9306 $this->get_context(),
9307 $this->show_intro(),
9308 $this->get_course_module()->id
9312 $confirm = optional_param('confirm', 0, PARAM_BOOL);
9314 if ($confirm) {
9315 confirm_sesskey();
9317 // Fix the grades.
9318 $this->fix_null_grades();
9319 unset_config('has_rescaled_null_grades_' . $instance->id, 'assign');
9321 // Display the notice.
9322 $o .= $this->get_renderer()->notification(get_string('fixrescalednullgradesdone', 'assign'), 'notifysuccess');
9323 $url = new moodle_url(
9324 '/mod/assign/view.php',
9325 array(
9326 'id' => $this->get_course_module()->id,
9327 'action' => 'grading'
9330 $o .= $this->get_renderer()->continue_button($url);
9331 } else {
9332 // Ask for confirmation.
9333 $continue = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades', 'confirm' => true, 'sesskey' => sesskey()));
9334 $cancel = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
9335 $o .= $OUTPUT->confirm(get_string('fixrescalednullgradesconfirm', 'mod_assign'), $continue, $cancel);
9338 $o .= $this->view_footer();
9340 return $o;
9344 * Set the most recent submission for the team.
9345 * The most recent team submission is used to determine if another attempt should be created when allowing another
9346 * attempt on a group assignment, and whether the gradebook should be updated.
9348 * @since Moodle 3.4
9349 * @param stdClass $submission The most recent submission of the group.
9351 public function set_most_recent_team_submission($submission) {
9352 $this->mostrecentteamsubmission = $submission;
9356 * Return array of valid grading allocation filters for the grading interface.
9358 * @param boolean $export Export the list of filters for a template.
9359 * @return array
9361 public function get_marking_allocation_filters($export = false) {
9362 $markingallocation = $this->get_instance()->markingworkflow &&
9363 $this->get_instance()->markingallocation &&
9364 has_capability('mod/assign:manageallocations', $this->context);
9365 // Get markers to use in drop lists.
9366 $markingallocationoptions = array();
9367 if ($markingallocation) {
9368 list($sort, $params) = users_order_by_sql('u');
9369 // Only enrolled users could be assigned as potential markers.
9370 $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
9371 $markingallocationoptions[''] = get_string('filternone', 'assign');
9372 $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
9373 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
9374 foreach ($markers as $marker) {
9375 $markingallocationoptions[$marker->id] = fullname($marker, $viewfullnames);
9378 if ($export) {
9379 $allocationfilter = get_user_preferences('assign_markerfilter', '');
9380 $result = [];
9381 foreach ($markingallocationoptions as $option => $label) {
9382 array_push($result, [
9383 'key' => $option,
9384 'name' => $label,
9385 'active' => ($allocationfilter == $option),
9388 return $result;
9390 return $markingworkflowoptions;
9394 * Return array of valid grading workflow filters for the grading interface.
9396 * @param boolean $export Export the list of filters for a template.
9397 * @return array
9399 public function get_marking_workflow_filters($export = false) {
9400 $markingworkflow = $this->get_instance()->markingworkflow;
9401 // Get marking states to show in form.
9402 $markingworkflowoptions = array();
9403 if ($markingworkflow) {
9404 $notmarked = get_string('markingworkflowstatenotmarked', 'assign');
9405 $markingworkflowoptions[''] = get_string('filternone', 'assign');
9406 $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked;
9407 $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user());
9409 if ($export) {
9410 $workflowfilter = get_user_preferences('assign_workflowfilter', '');
9411 $result = [];
9412 foreach ($markingworkflowoptions as $option => $label) {
9413 array_push($result, [
9414 'key' => $option,
9415 'name' => $label,
9416 'active' => ($workflowfilter == $option),
9419 return $result;
9421 return $markingworkflowoptions;
9425 * Return array of valid search filters for the grading interface.
9427 * @return array
9429 public function get_filters() {
9430 $filterkeys = [
9431 ASSIGN_FILTER_NOT_SUBMITTED,
9432 ASSIGN_FILTER_DRAFT,
9433 ASSIGN_FILTER_SUBMITTED,
9434 ASSIGN_FILTER_REQUIRE_GRADING,
9435 ASSIGN_FILTER_GRANTED_EXTENSION
9438 $current = get_user_preferences('assign_filter', '');
9440 $filters = [];
9441 // First is always "no filter" option.
9442 array_push($filters, [
9443 'key' => 'none',
9444 'name' => get_string('filternone', 'assign'),
9445 'active' => ($current == '')
9448 foreach ($filterkeys as $key) {
9449 array_push($filters, [
9450 'key' => $key,
9451 'name' => get_string('filter' . $key, 'assign'),
9452 'active' => ($current == $key)
9455 return $filters;
9459 * Get the correct submission statement depending on single submisison, team submission or team submission
9460 * where all team memebers must submit.
9462 * @param array $adminconfig
9463 * @param assign $instance
9464 * @param context $context
9466 * @return string
9468 protected function get_submissionstatement($adminconfig, $instance, $context) {
9469 $submissionstatement = '';
9471 if (!($context instanceof context)) {
9472 return $submissionstatement;
9475 // Single submission.
9476 if (!$instance->teamsubmission) {
9477 // Single submission statement is not empty.
9478 if (!empty($adminconfig->submissionstatement)) {
9479 // Format the submission statement before its sent. We turn off para because this is going within
9480 // a form element.
9481 $options = array(
9482 'context' => $context,
9483 'para' => false
9485 $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
9487 } else { // Team submission.
9488 // One user can submit for the whole team.
9489 if (!empty($adminconfig->submissionstatementteamsubmission) && !$instance->requireallteammemberssubmit) {
9490 // Format the submission statement before its sent. We turn off para because this is going within
9491 // a form element.
9492 $options = array(
9493 'context' => $context,
9494 'para' => false
9496 $submissionstatement = format_text($adminconfig->submissionstatementteamsubmission,
9497 FORMAT_MOODLE, $options);
9498 } else if (!empty($adminconfig->submissionstatementteamsubmissionallsubmit) &&
9499 $instance->requireallteammemberssubmit) {
9500 // All team members must submit.
9501 // Format the submission statement before its sent. We turn off para because this is going within
9502 // a form element.
9503 $options = array(
9504 'context' => $context,
9505 'para' => false
9507 $submissionstatement = format_text($adminconfig->submissionstatementteamsubmissionallsubmit,
9508 FORMAT_MOODLE, $options);
9512 return $submissionstatement;
9517 * Portfolio caller class for mod_assign.
9519 * @package mod_assign
9520 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
9521 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
9523 class assign_portfolio_caller extends portfolio_module_caller_base {
9525 /** @var int callback arg - the id of submission we export */
9526 protected $sid;
9528 /** @var string component of the submission files we export*/
9529 protected $component;
9531 /** @var string callback arg - the area of submission files we export */
9532 protected $area;
9534 /** @var int callback arg - the id of file we export */
9535 protected $fileid;
9537 /** @var int callback arg - the cmid of the assignment we export */
9538 protected $cmid;
9540 /** @var string callback arg - the plugintype of the editor we export */
9541 protected $plugin;
9543 /** @var string callback arg - the name of the editor field we export */
9544 protected $editor;
9547 * Callback arg for a single file export.
9549 public static function expected_callbackargs() {
9550 return array(
9551 'cmid' => true,
9552 'sid' => false,
9553 'area' => false,
9554 'component' => false,
9555 'fileid' => false,
9556 'plugin' => false,
9557 'editor' => false,
9562 * The constructor.
9564 * @param array $callbackargs
9566 public function __construct($callbackargs) {
9567 parent::__construct($callbackargs);
9568 $this->cm = get_coursemodule_from_id('assign', $this->cmid, 0, false, MUST_EXIST);
9572 * Load data needed for the portfolio export.
9574 * If the assignment type implements portfolio_load_data(), the processing is delegated
9575 * to it. Otherwise, the caller must provide either fileid (to export single file) or
9576 * submissionid and filearea (to export all data attached to the given submission file area)
9577 * via callback arguments.
9579 * @throws portfolio_caller_exception
9581 public function load_data() {
9582 global $DB;
9584 $context = context_module::instance($this->cmid);
9586 if (empty($this->fileid)) {
9587 if (empty($this->sid) || empty($this->area)) {
9588 throw new portfolio_caller_exception('invalidfileandsubmissionid', 'mod_assign');
9591 $submission = $DB->get_record('assign_submission', array('id' => $this->sid));
9592 } else {
9593 $submissionid = $DB->get_field('files', 'itemid', array('id' => $this->fileid, 'contextid' => $context->id));
9594 if ($submissionid) {
9595 $submission = $DB->get_record('assign_submission', array('id' => $submissionid));
9599 if (empty($submission)) {
9600 throw new portfolio_caller_exception('filenotfound');
9601 } else if ($submission->userid == 0) {
9602 // This must be a group submission.
9603 if (!groups_is_member($submission->groupid, $this->user->id)) {
9604 throw new portfolio_caller_exception('filenotfound');
9606 } else if ($this->user->id != $submission->userid) {
9607 throw new portfolio_caller_exception('filenotfound');
9610 // Export either an area of files or a single file (see function for more detail).
9611 // The first arg is an id or null. If it is an id, the rest of the args are ignored.
9612 // If it is null, the rest of the args are used to load a list of files from get_areafiles.
9613 $this->set_file_and_format_data($this->fileid,
9614 $context->id,
9615 $this->component,
9616 $this->area,
9617 $this->sid,
9618 'timemodified',
9619 false);
9624 * Prepares the package up before control is passed to the portfolio plugin.
9626 * @throws portfolio_caller_exception
9627 * @return mixed
9629 public function prepare_package() {
9631 if ($this->plugin && $this->editor) {
9632 $options = portfolio_format_text_options();
9633 $context = context_module::instance($this->cmid);
9634 $options->context = $context;
9636 $plugin = $this->get_submission_plugin();
9638 $text = $plugin->get_editor_text($this->editor, $this->sid);
9639 $format = $plugin->get_editor_format($this->editor, $this->sid);
9641 $html = format_text($text, $format, $options);
9642 $html = portfolio_rewrite_pluginfile_urls($html,
9643 $context->id,
9644 'mod_assign',
9645 $this->area,
9646 $this->sid,
9647 $this->exporter->get('format'));
9649 $exporterclass = $this->exporter->get('formatclass');
9650 if (in_array($exporterclass, array(PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_RICHHTML))) {
9651 if ($files = $this->exporter->get('caller')->get('multifiles')) {
9652 foreach ($files as $file) {
9653 $this->exporter->copy_existing_file($file);
9656 return $this->exporter->write_new_file($html, 'assignment.html', !empty($files));
9657 } else if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
9658 $leapwriter = $this->exporter->get('format')->leap2a_writer();
9659 $entry = new portfolio_format_leap2a_entry($this->area . $this->cmid,
9660 $context->get_context_name(),
9661 'resource',
9662 $html);
9664 $entry->add_category('web', 'resource_type');
9665 $entry->author = $this->user;
9666 $leapwriter->add_entry($entry);
9667 if ($files = $this->exporter->get('caller')->get('multifiles')) {
9668 $leapwriter->link_files($entry, $files, $this->area . $this->cmid . 'file');
9669 foreach ($files as $file) {
9670 $this->exporter->copy_existing_file($file);
9673 return $this->exporter->write_new_file($leapwriter->to_xml(),
9674 $this->exporter->get('format')->manifest_name(),
9675 true);
9676 } else {
9677 debugging('invalid format class: ' . $this->exporter->get('formatclass'));
9682 if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
9683 $leapwriter = $this->exporter->get('format')->leap2a_writer();
9684 $files = array();
9685 if ($this->singlefile) {
9686 $files[] = $this->singlefile;
9687 } else if ($this->multifiles) {
9688 $files = $this->multifiles;
9689 } else {
9690 throw new portfolio_caller_exception('invalidpreparepackagefile',
9691 'portfolio',
9692 $this->get_return_url());
9695 $entryids = array();
9696 foreach ($files as $file) {
9697 $entry = new portfolio_format_leap2a_file($file->get_filename(), $file);
9698 $entry->author = $this->user;
9699 $leapwriter->add_entry($entry);
9700 $this->exporter->copy_existing_file($file);
9701 $entryids[] = $entry->id;
9703 if (count($files) > 1) {
9704 $baseid = 'assign' . $this->cmid . $this->area;
9705 $context = context_module::instance($this->cmid);
9707 // If we have multiple files, they should be grouped together into a folder.
9708 $entry = new portfolio_format_leap2a_entry($baseid . 'group',
9709 $context->get_context_name(),
9710 'selection');
9711 $leapwriter->add_entry($entry);
9712 $leapwriter->make_selection($entry, $entryids, 'Folder');
9714 return $this->exporter->write_new_file($leapwriter->to_xml(),
9715 $this->exporter->get('format')->manifest_name(),
9716 true);
9718 return $this->prepare_package_file();
9722 * Fetch the plugin by its type.
9724 * @return assign_submission_plugin
9726 protected function get_submission_plugin() {
9727 global $CFG;
9728 if (!$this->plugin || !$this->cmid) {
9729 return null;
9732 require_once($CFG->dirroot . '/mod/assign/locallib.php');
9734 $context = context_module::instance($this->cmid);
9736 $assignment = new assign($context, null, null);
9737 return $assignment->get_submission_plugin_by_type($this->plugin);
9741 * Calculate a sha1 has of either a single file or a list
9742 * of files based on the data set by load_data.
9744 * @return string
9746 public function get_sha1() {
9748 if ($this->plugin && $this->editor) {
9749 $plugin = $this->get_submission_plugin();
9750 $options = portfolio_format_text_options();
9751 $options->context = context_module::instance($this->cmid);
9753 $text = format_text($plugin->get_editor_text($this->editor, $this->sid),
9754 $plugin->get_editor_format($this->editor, $this->sid),
9755 $options);
9756 $textsha1 = sha1($text);
9757 $filesha1 = '';
9758 try {
9759 $filesha1 = $this->get_sha1_file();
9760 } catch (portfolio_caller_exception $e) {
9761 // No files.
9763 return sha1($textsha1 . $filesha1);
9765 return $this->get_sha1_file();
9769 * Calculate the time to transfer either a single file or a list
9770 * of files based on the data set by load_data.
9772 * @return int
9774 public function expected_time() {
9775 return $this->expected_time_file();
9779 * Checking the permissions.
9781 * @return bool
9783 public function check_permissions() {
9784 $context = context_module::instance($this->cmid);
9785 return has_capability('mod/assign:exportownsubmission', $context);
9789 * Display a module name.
9791 * @return string
9793 public static function display_name() {
9794 return get_string('modulename', 'assign');
9798 * Return array of formats supported by this portfolio call back.
9800 * @return array
9802 public static function base_supported_formats() {
9803 return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_LEAP2A);
9808 * Logic to happen when a/some group(s) has/have been deleted in a course.
9810 * @param int $courseid The course ID.
9811 * @param int $groupid The group id if it is known
9812 * @return void
9814 function assign_process_group_deleted_in_course($courseid, $groupid = null) {
9815 global $DB;
9817 $params = array('courseid' => $courseid);
9818 if ($groupid) {
9819 $params['groupid'] = $groupid;
9820 // We just update the group that was deleted.
9821 $sql = "SELECT o.id, o.assignid, o.groupid
9822 FROM {assign_overrides} o
9823 JOIN {assign} assign ON assign.id = o.assignid
9824 WHERE assign.course = :courseid
9825 AND o.groupid = :groupid";
9826 } else {
9827 // No groupid, we update all orphaned group overrides for all assign in course.
9828 $sql = "SELECT o.id, o.assignid, o.groupid
9829 FROM {assign_overrides} o
9830 JOIN {assign} assign ON assign.id = o.assignid
9831 LEFT JOIN {groups} grp ON grp.id = o.groupid
9832 WHERE assign.course = :courseid
9833 AND o.groupid IS NOT NULL
9834 AND grp.id IS NULL";
9836 $records = $DB->get_records_sql($sql, $params);
9837 if (!$records) {
9838 return; // Nothing to do.
9840 $DB->delete_records_list('assign_overrides', 'id', array_keys($records));
9841 $cache = cache::make('mod_assign', 'overrides');
9842 foreach ($records as $record) {
9843 $cache->delete("{$record->assignid}_g_{$record->groupid}");
9848 * Change the sort order of an override
9850 * @param int $id of the override
9851 * @param string $move direction of move
9852 * @param int $assignid of the assignment
9853 * @return bool success of operation
9855 function move_group_override($id, $move, $assignid) {
9856 global $DB;
9858 // Get the override object.
9859 if (!$override = $DB->get_record('assign_overrides', ['id' => $id], 'id, sortorder, groupid')) {
9860 return false;
9862 // Count the number of group overrides.
9863 $overridecountgroup = $DB->count_records('assign_overrides', array('userid' => null, 'assignid' => $assignid));
9865 // Calculate the new sortorder.
9866 if ( ($move == 'up') and ($override->sortorder > 1)) {
9867 $neworder = $override->sortorder - 1;
9868 } else if (($move == 'down') and ($override->sortorder < $overridecountgroup)) {
9869 $neworder = $override->sortorder + 1;
9870 } else {
9871 return false;
9874 // Retrieve the override object that is currently residing in the new position.
9875 $params = ['sortorder' => $neworder, 'assignid' => $assignid];
9876 if ($swapoverride = $DB->get_record('assign_overrides', $params, 'id, sortorder, groupid')) {
9878 // Swap the sortorders.
9879 $swapoverride->sortorder = $override->sortorder;
9880 $override->sortorder = $neworder;
9882 // Update the override records.
9883 $DB->update_record('assign_overrides', $override);
9884 $DB->update_record('assign_overrides', $swapoverride);
9886 // Delete cache for the 2 records we updated above.
9887 $cache = cache::make('mod_assign', 'overrides');
9888 $cache->delete("{$override->assignid}_g_{$override->groupid}");
9889 $cache->delete("{$swapoverride->assignid}_g_{$swapoverride->groupid}");
9892 reorder_group_overrides($assignid);
9893 return true;
9897 * Reorder the overrides starting at the override at the given startorder.
9899 * @param int $assignid of the assigment
9901 function reorder_group_overrides($assignid) {
9902 global $DB;
9904 $i = 1;
9905 if ($overrides = $DB->get_records('assign_overrides', array('userid' => null, 'assignid' => $assignid), 'sortorder ASC')) {
9906 $cache = cache::make('mod_assign', 'overrides');
9907 foreach ($overrides as $override) {
9908 $f = new stdClass();
9909 $f->id = $override->id;
9910 $f->sortorder = $i++;
9911 $DB->update_record('assign_overrides', $f);
9912 $cache->delete("{$assignid}_g_{$override->groupid}");
9914 // Update priorities of group overrides.
9915 $params = [
9916 'modulename' => 'assign',
9917 'instance' => $override->assignid,
9918 'groupid' => $override->groupid
9920 $DB->set_field('event', 'priority', $f->sortorder, $params);