weekly release 5.0dev
[moodle.git] / mod / assign / locallib.php
blob46588ccbd1f29e616f79180b4bca9bcc3f1d952a
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 /**
49 * ASSIGN_ATTEMPT_REOPEN_METHOD_NONE - Reopening attempts is not allowed.
51 * @deprecated since Moodle 4.4
52 * @todo MDL-81977 This will be deleted in Moodle 4.8.
54 define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none');
55 define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual');
56 define('ASSIGN_ATTEMPT_REOPEN_METHOD_AUTOMATIC', 'automatic');
57 define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass');
59 // Special value means allow unlimited attempts.
60 define('ASSIGN_UNLIMITED_ATTEMPTS', -1);
62 // Special value means no grade has been set.
63 define('ASSIGN_GRADE_NOT_SET', -1);
65 // Grading states.
66 define('ASSIGN_GRADING_STATUS_GRADED', 'graded');
67 define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded');
69 // Marking workflow states.
70 define('ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED', 'notmarked');
71 define('ASSIGN_MARKING_WORKFLOW_STATE_INMARKING', 'inmarking');
72 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW', 'readyforreview');
73 define('ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW', 'inreview');
74 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE', 'readyforrelease');
75 define('ASSIGN_MARKING_WORKFLOW_STATE_RELEASED', 'released');
77 /** ASSIGN_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
78 define("ASSIGN_MAX_EVENT_LENGTH", "432000");
80 // Name of file area for intro attachments.
81 define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment');
83 // Name of file area for activity attachments.
84 define('ASSIGN_ACTIVITYATTACHMENT_FILEAREA', 'activityattachment');
86 // Event types.
87 define('ASSIGN_EVENT_TYPE_DUE', 'due');
88 define('ASSIGN_EVENT_TYPE_GRADINGDUE', 'gradingdue');
89 define('ASSIGN_EVENT_TYPE_OPEN', 'open');
90 define('ASSIGN_EVENT_TYPE_CLOSE', 'close');
91 define('ASSIGN_EVENT_TYPE_EXTENSION', 'extension');
93 require_once($CFG->libdir . '/accesslib.php');
94 require_once($CFG->libdir . '/formslib.php');
95 require_once($CFG->dirroot . '/repository/lib.php');
96 require_once($CFG->dirroot . '/mod/assign/mod_form.php');
97 require_once($CFG->libdir . '/gradelib.php');
98 require_once($CFG->dirroot . '/grade/grading/lib.php');
99 require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php');
100 require_once($CFG->dirroot . '/mod/assign/submissionplugin.php');
101 require_once($CFG->dirroot . '/mod/assign/renderable.php');
102 require_once($CFG->dirroot . '/mod/assign/gradingtable.php');
103 require_once($CFG->libdir . '/portfolio/caller.php');
105 use mod_assign\event\submission_removed;
106 use mod_assign\event\submission_status_updated;
107 use \mod_assign\output\grading_app;
108 use \mod_assign\output\assign_header;
109 use \mod_assign\output\assign_submission_status;
110 use mod_assign\output\timelimit_panel;
111 use mod_assign\downloader;
114 * Standard base class for mod_assign (assignment types).
116 * @package mod_assign
117 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
118 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
120 class assign {
122 /** @var stdClass the assignment record that contains the global settings for this assign instance */
123 private $instance;
125 /** @var array $var array an array containing per-user assignment records, each having calculated properties (e.g. dates) */
126 private $userinstances = [];
128 /** @var grade_item the grade_item record for this assign instance's primary grade item. */
129 private $gradeitem;
131 /** @var context the context of the course module for this assign instance
132 * (or just the course if we are creating a new one)
134 private $context;
136 /** @var stdClass the course this assign instance belongs to */
137 private $course;
139 /** @var stdClass the admin config for all assign instances */
140 private $adminconfig;
142 /** @var assign_renderer the custom renderer for this module */
143 private $output;
145 /** @var cm_info the course module for this assign instance */
146 private $coursemodule;
148 /** @var array cache for things like the coursemodule name or the scale menu -
149 * only lives for a single request.
151 private $cache;
153 /** @var array list of the installed submission plugins */
154 private $submissionplugins;
156 /** @var array list of the installed feedback plugins */
157 private $feedbackplugins;
159 /** @var string action to be used to return to this page
160 * (without repeating any form submissions etc).
162 private $returnaction = 'view';
164 /** @var array params to be used to return to this page */
165 private $returnparams = array();
167 /** @var string modulename prevents excessive calls to get_string */
168 private static $modulename = null;
170 /** @var string modulenameplural prevents excessive calls to get_string */
171 private static $modulenameplural = null;
173 /** @var array of marking workflow states for the current user */
174 private $markingworkflowstates = null;
176 /** @var array of all marking workflow states */
177 private $allmarkingworkflowstates = null;
179 /** @var bool whether to exclude users with inactive enrolment */
180 private $showonlyactiveenrol = null;
182 /** @var string A key used to identify userlists created by this object. */
183 private $useridlistid = null;
185 /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */
186 private $participants = array();
188 /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */
189 private $usersubmissiongroups = array();
191 /** @var array cached list of user groups. The cache key will be the user. */
192 private $usergroups = array();
194 /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */
195 private $sharedgroupmembers = array();
198 * @var stdClass The most recent team submission. Used to determine additional attempt numbers and whether
199 * to update the gradebook.
201 private $mostrecentteamsubmission = null;
203 /** @var array Array of error messages encountered during the execution of assignment related operations. */
204 private $errors = array();
206 /** @var mixed This var can vary between false for no overrides to a stdClass of the overrides for a group */
207 private $overridedata;
209 /** @var float grade value. */
210 public $grade;
212 /** @var array $usersearch The content that the current user is looking for. */
213 protected array $usersearch = [];
216 * Constructor for the base assign class.
218 * Note: For $coursemodule you can supply a stdclass if you like, but it
219 * will be more efficient to supply a cm_info object.
221 * @param mixed $coursemodulecontext context|null the course module context
222 * (or the course context if the coursemodule has not been
223 * created yet).
224 * @param mixed $coursemodule the current course module if it was already loaded,
225 * otherwise this class will load one from the context as required.
226 * @param mixed $course the current course if it was already loaded,
227 * otherwise this class will load one from the context as required.
229 public function __construct($coursemodulecontext, $coursemodule, $course) {
230 $this->context = $coursemodulecontext;
231 $this->course = $course;
233 // Ensure that $this->coursemodule is a cm_info object (or null).
234 $this->coursemodule = cm_info::create($coursemodule);
236 // Temporary cache only lives for a single request - used to reduce db lookups.
237 $this->cache = array();
239 $this->submissionplugins = $this->load_plugins('assignsubmission');
240 $this->feedbackplugins = $this->load_plugins('assignfeedback');
242 // Extra entropy is required for uniqid() to work on cygwin.
243 $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM);
247 * Set the action and parameters that can be used to return to the current page.
249 * @param string $action The action for the current page
250 * @param array $params An array of name value pairs which form the parameters
251 * to return to the current page.
252 * @return void
254 public function register_return_link($action, $params) {
255 global $PAGE;
256 $params['action'] = $action;
257 $cm = $this->get_course_module();
258 if ($cm) {
259 $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $cm->id));
260 } else {
261 $currenturl = new moodle_url('/mod/assign/index.php', array('id' => $this->get_course()->id));
264 $currenturl->params($params);
265 $PAGE->set_url($currenturl);
269 * Return an action that can be used to get back to the current page.
271 * @return string action
273 public function get_return_action() {
274 global $PAGE;
276 // Web services don't set a URL, we should avoid debugging when ussing the url object.
277 if (!WS_SERVER) {
278 $params = $PAGE->url->params();
281 if (!empty($params['action'])) {
282 return $params['action'];
284 return '';
288 * Based on the current assignment settings should we display the intro.
290 * @return bool showintro
292 public function show_intro() {
293 if ($this->get_instance()->alwaysshowdescription ||
294 time() > $this->get_instance()->allowsubmissionsfromdate) {
295 return true;
297 return false;
301 * Return a list of parameters that can be used to get back to the current page.
303 * @return array params
305 public function get_return_params() {
306 global $PAGE;
308 $params = array();
309 if (!WS_SERVER) {
310 $params = $PAGE->url->params();
312 unset($params['id']);
313 unset($params['action']);
314 return $params;
318 * Set the submitted form data.
320 * @param stdClass $data The form data (instance)
322 public function set_instance(stdClass $data) {
323 $this->instance = $data;
327 * Set the context.
329 * @param context $context The new context
331 public function set_context(context $context) {
332 $this->context = $context;
336 * Set the course data.
338 * @param stdClass $course The course data
340 public function set_course(stdClass $course) {
341 $this->course = $course;
345 * Set usersearch to limit results when getting list of participants.
347 * @param int|null $userid User id to search for.
348 * @param int|null $groupid Group id to limit resuts to specific group.
349 * @param string $usersearch Search string to limit results.
351 public function set_usersearch(?int $userid, ?int $groupid, string $usersearch = ''): void {
352 $usersearcharray = [];
353 $usersearcharray['userid'] = $userid;
354 $usersearcharray['groupid'] = $groupid;
355 $usersearcharray['usersearch'] = $usersearch;
356 $this->usersearch = $usersearcharray;
360 * Set error message.
362 * @param string $message The error message
364 protected function set_error_message(string $message) {
365 $this->errors[] = $message;
369 * Get error messages.
371 * @return array The array of error messages
373 public function get_error_messages(): array {
374 return $this->errors;
378 * Get list of feedback plugins installed.
380 * @return array
382 public function get_feedback_plugins() {
383 return $this->feedbackplugins;
387 * Get list of submission plugins installed.
389 * @return array
391 public function get_submission_plugins() {
392 return $this->submissionplugins;
396 * Is blind marking enabled and reveal identities not set yet?
398 * @return bool
400 public function is_blind_marking() {
401 return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities;
405 * Is hidden grading enabled?
407 * This just checks the assignment settings. Remember to check
408 * the user has the 'showhiddengrader' capability too
410 * @return bool
412 public function is_hidden_grader() {
413 return $this->get_instance()->hidegrader;
417 * Does an assignment have submission(s) or grade(s) already?
419 * @return bool
421 public function has_submissions_or_grades() {
422 $allgrades = $this->count_grades();
423 $allsubmissions = $this->count_submissions();
424 if (($allgrades == 0) && ($allsubmissions == 0)) {
425 return false;
427 return true;
431 * Get a specific submission plugin by its type.
433 * @param string $subtype assignsubmission | assignfeedback
434 * @param string $type
435 * @return mixed assign_plugin|null
437 public function get_plugin_by_type($subtype, $type) {
438 $shortsubtype = substr($subtype, strlen('assign'));
439 $name = $shortsubtype . 'plugins';
440 if ($name != 'feedbackplugins' && $name != 'submissionplugins') {
441 return null;
443 $pluginlist = $this->$name;
444 foreach ($pluginlist as $plugin) {
445 if ($plugin->get_type() == $type) {
446 return $plugin;
449 return null;
453 * Get a feedback plugin by type.
455 * @param string $type - The type of plugin e.g comments
456 * @return mixed assign_feedback_plugin|null
458 public function get_feedback_plugin_by_type($type) {
459 return $this->get_plugin_by_type('assignfeedback', $type);
463 * Get a submission plugin by type.
465 * @param string $type - The type of plugin e.g comments
466 * @return mixed assign_submission_plugin|null
468 public function get_submission_plugin_by_type($type) {
469 return $this->get_plugin_by_type('assignsubmission', $type);
473 * Load the plugins from the sub folders under subtype.
475 * @param string $subtype - either submission or feedback
476 * @return array - The sorted list of plugins
478 public function load_plugins($subtype) {
479 global $CFG;
480 $result = array();
482 $names = core_component::get_plugin_list($subtype);
484 foreach ($names as $name => $path) {
485 if (file_exists($path . '/locallib.php')) {
486 require_once($path . '/locallib.php');
488 $shortsubtype = substr($subtype, strlen('assign'));
489 $pluginclass = 'assign_' . $shortsubtype . '_' . $name;
491 $plugin = new $pluginclass($this, $name);
493 if ($plugin instanceof assign_plugin) {
494 $idx = $plugin->get_sort_order();
495 while (array_key_exists($idx, $result)) {
496 $idx +=1;
498 $result[$idx] = $plugin;
502 ksort($result);
503 return $result;
507 * Display the assignment, used by view.php
509 * The assignment is displayed differently depending on your role,
510 * the settings for the assignment and the status of the assignment.
512 * @param string $action The current action if any.
513 * @param array $args Optional arguments to pass to the view (instead of getting them from GET and POST).
514 * @return string - The page output.
516 public function view($action='', $args = array()) {
517 global $PAGE;
519 $o = '';
520 $mform = null;
521 $notices = array();
522 $nextpageparams = array();
524 if (!empty($this->get_course_module()->id)) {
525 $nextpageparams['id'] = $this->get_course_module()->id;
528 if (empty($action)) {
529 $PAGE->add_body_class('limitedwidth');
532 // Handle form submissions first.
533 if ($action == 'savesubmission') {
534 $action = 'editsubmission';
535 if ($this->process_save_submission($mform, $notices)) {
536 $action = 'redirect';
537 if ($this->can_grade()) {
538 $nextpageparams['action'] = 'grading';
539 } else {
540 $nextpageparams['action'] = 'view';
543 } else if ($action == 'editprevioussubmission') {
544 $action = 'editsubmission';
545 if ($this->process_copy_previous_attempt($notices)) {
546 $action = 'redirect';
547 $nextpageparams['action'] = 'editsubmission';
549 } else if ($action == 'lock') {
550 $this->process_lock_submission();
551 $action = 'redirect';
552 $nextpageparams['action'] = 'grading';
553 } else if ($action == 'removesubmission') {
554 $this->process_remove_submission();
555 $action = 'redirect';
556 if ($this->can_grade()) {
557 $nextpageparams['action'] = 'grading';
558 } else {
559 $nextpageparams['action'] = 'view';
561 } else if ($action == 'addattempt') {
562 $this->process_add_attempt(required_param('userid', PARAM_INT));
563 $action = 'redirect';
564 $nextpageparams['action'] = 'grading';
565 } else if ($action == 'reverttodraft') {
566 $this->process_revert_to_draft();
567 $action = 'redirect';
568 $nextpageparams['action'] = 'grading';
569 } else if ($action == 'unlock') {
570 $this->process_unlock_submission();
571 $action = 'redirect';
572 $nextpageparams['action'] = 'grading';
573 } else if ($action == 'setbatchmarkingworkflowstate') {
574 $this->process_set_batch_marking_workflow_state();
575 $action = 'redirect';
576 $nextpageparams['action'] = 'grading';
577 } else if ($action == 'setbatchmarkingallocation') {
578 $this->process_set_batch_marking_allocation();
579 $action = 'redirect';
580 $nextpageparams['action'] = 'grading';
581 } else if ($action == 'confirmsubmit') {
582 $action = 'submit';
583 if ($this->process_submit_for_grading($mform, $notices)) {
584 $action = 'redirect';
585 $nextpageparams['action'] = 'view';
586 } else if ($notices) {
587 $action = 'viewsubmitforgradingerror';
589 } else if ($action == 'submitotherforgrading') {
590 if ($this->process_submit_other_for_grading($mform, $notices)) {
591 $action = 'redirect';
592 $nextpageparams['action'] = 'grading';
593 } else {
594 $action = 'viewsubmitforgradingerror';
596 } else if ($action == 'gradingbatchoperation') {
597 $action = $this->process_grading_batch_operation();
598 if ($action == 'grading') {
599 $action = 'redirect';
600 $nextpageparams['action'] = 'grading';
602 } else if ($action == 'submitgrade') {
603 if (optional_param('saveandshownext', null, PARAM_RAW)) {
604 // Save and show next.
605 $action = 'grade';
606 if ($this->process_save_grade($mform)) {
607 $action = 'redirect';
608 $nextpageparams['action'] = 'grade';
609 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
610 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
612 } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) {
613 $action = 'redirect';
614 $nextpageparams['action'] = 'grade';
615 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1;
616 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
617 } else if (optional_param('nosaveandnext', null, PARAM_RAW)) {
618 $action = 'redirect';
619 $nextpageparams['action'] = 'grade';
620 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
621 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
622 } else if (optional_param('savegrade', null, PARAM_RAW)) {
623 // Save changes button.
624 $action = 'grade';
625 if ($this->process_save_grade($mform)) {
626 $action = 'redirect';
627 $nextpageparams['action'] = 'savegradingresult';
629 } else {
630 // Cancel button.
631 $action = 'redirect';
632 $nextpageparams['action'] = 'grading';
634 } else if ($action == 'quickgrade') {
635 $message = $this->process_save_quick_grades();
636 $action = 'quickgradingresult';
637 } else if ($action == 'saveextension') {
638 $action = 'grantextension';
639 if ($this->process_save_extension($mform)) {
640 $action = 'redirect';
641 $nextpageparams['action'] = 'grading';
643 } else if ($action == 'revealidentitiesconfirm') {
644 $this->process_reveal_identities();
645 $action = 'redirect';
646 $nextpageparams['action'] = 'grading';
649 $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT),
650 'useridlistid' => optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM));
651 $this->register_return_link($action, $returnparams);
653 // Include any page action as part of the body tag CSS id.
654 if (!empty($action)) {
655 $PAGE->set_pagetype('mod-assign-' . $action);
657 // Now show the right view page.
658 if ($action == 'redirect') {
659 $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams);
660 $messages = '';
661 $messagetype = \core\output\notification::NOTIFY_INFO;
662 $errors = $this->get_error_messages();
663 if (!empty($errors)) {
664 $messages = html_writer::alist($errors, ['class' => 'mb-1 mt-1']);
665 $messagetype = \core\output\notification::NOTIFY_ERROR;
667 redirect($nextpageurl, $messages, null, $messagetype);
668 return;
669 } else if ($action == 'savegradingresult') {
670 $message = get_string('gradingchangessaved', 'assign');
671 $o .= $this->view_savegrading_result($message);
672 } else if ($action == 'quickgradingresult') {
673 $mform = null;
674 $o .= $this->view_quickgrading_result($message);
675 } else if ($action == 'gradingpanel') {
676 $o .= $this->view_single_grading_panel($args);
677 } else if ($action == 'grade') {
678 $o .= $this->view_single_grade_page($mform);
679 } else if ($action == 'viewpluginassignfeedback') {
680 $o .= $this->view_plugin_content('assignfeedback');
681 } else if ($action == 'viewpluginassignsubmission') {
682 $o .= $this->view_plugin_content('assignsubmission');
683 } else if ($action == 'editsubmission') {
684 $PAGE->add_body_class('limitedwidth');
685 $o .= $this->view_edit_submission_page($mform, $notices);
686 } else if ($action == 'grader') {
687 $o .= $this->view_grader();
688 } else if ($action == 'grading') {
689 $o .= $this->view_grading_page();
690 } else if ($action == 'downloadall') {
691 $o .= $this->download_submissions();
692 } else if ($action == 'submit') {
693 $PAGE->add_body_class('limitedwidth');
694 $o .= $this->check_submit_for_grading($mform);
695 } else if ($action == 'grantextension') {
696 $o .= $this->view_grant_extension($mform);
697 } else if ($action == 'revealidentities') {
698 $o .= $this->view_reveal_identities_confirm();
699 } else if ($action == 'removesubmissionconfirm') {
700 $PAGE->add_body_class('limitedwidth');
701 $o .= $this->view_remove_submission_confirm();
702 } else if ($action == 'plugingradingbatchoperation') {
703 $o .= $this->view_plugin_grading_batch_operation();
704 } else if ($action == 'viewpluginpage') {
705 $o .= $this->view_plugin_page();
706 } else if ($action == 'viewcourseindex') {
707 $o .= $this->view_course_index();
708 } else if ($action == 'viewbatchsetmarkingworkflowstate') {
709 $o .= $this->view_batch_set_workflow_state();
710 } else if ($action == 'viewbatchmarkingallocation') {
711 $o .= $this->view_batch_markingallocation();
712 } else if ($action == 'viewsubmitforgradingerror') {
713 $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices);
714 } else if ($action == 'fixrescalednullgrades') {
715 $o .= $this->view_fix_rescaled_null_grades();
716 } else {
717 $PAGE->add_body_class('limitedwidth');
718 $o .= $this->view_submission_page();
721 return $o;
725 * Add this instance to the database.
727 * @param stdClass $formdata The data submitted from the form
728 * @param bool $callplugins This is used to skip the plugin code
729 * when upgrading an old assignment to a new one (the plugins get called manually)
730 * @return mixed false if an error occurs or the int id of the new instance
732 public function add_instance(stdClass $formdata, $callplugins) {
733 global $DB;
734 $adminconfig = $this->get_admin_config();
736 $err = '';
738 // Add the database record.
739 $update = new stdClass();
740 $update->name = $formdata->name;
741 $update->timemodified = time();
742 $update->timecreated = time();
743 $update->course = $formdata->course;
744 $update->courseid = $formdata->course;
745 $update->intro = $formdata->intro;
746 $update->introformat = $formdata->introformat;
747 $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
748 if (isset($formdata->activityeditor)) {
749 $update->activity = $this->save_editor_draft_files($formdata);
750 $update->activityformat = $formdata->activityeditor['format'];
752 if (isset($formdata->submissionattachments)) {
753 $update->submissionattachments = $formdata->submissionattachments;
755 $update->submissiondrafts = $formdata->submissiondrafts;
756 $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
757 $update->sendnotifications = $formdata->sendnotifications;
758 $update->sendlatenotifications = $formdata->sendlatenotifications;
759 $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
760 if (isset($formdata->sendstudentnotifications)) {
761 $update->sendstudentnotifications = $formdata->sendstudentnotifications;
763 $update->duedate = $formdata->duedate;
764 $update->cutoffdate = $formdata->cutoffdate;
765 $update->gradingduedate = $formdata->gradingduedate;
766 if (isset($formdata->timelimit)) {
767 $update->timelimit = $formdata->timelimit;
769 $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
770 $update->grade = $formdata->grade;
771 $update->completionsubmit = !empty($formdata->completionsubmit);
772 $update->teamsubmission = $formdata->teamsubmission;
773 $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
774 if (isset($formdata->teamsubmissiongroupingid)) {
775 $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
777 $update->blindmarking = $formdata->blindmarking;
778 if (isset($formdata->hidegrader)) {
779 $update->hidegrader = $formdata->hidegrader;
781 $update->maxattempts = $formdata->maxattempts ?? 1;
782 $update->attemptreopenmethod = $formdata->attemptreopenmethod ?? ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS;
783 if (isset($formdata->preventsubmissionnotingroup)) {
784 $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
786 $update->markingworkflow = $formdata->markingworkflow;
787 $update->markingallocation = $formdata->markingallocation;
788 if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
789 $update->markingallocation = 0;
791 if (isset($formdata->markinganonymous)) {
792 // If marking workflow is disabled, or anonymous submissions is disabled then make sure marking anonymous is disabled.
793 if (empty($update->markingworkflow) || empty($update->blindmarking)) {
794 $update->markinganonymous = 0;
795 } else {
796 $update->markinganonymous = $formdata->markinganonymous;
799 $returnid = $DB->insert_record('assign', $update);
800 $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
801 // Cache the course record.
802 $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST);
804 $this->save_intro_draft_files($formdata);
805 $this->save_editor_draft_files($formdata);
807 if ($callplugins) {
808 // Call save_settings hook for submission plugins.
809 foreach ($this->submissionplugins as $plugin) {
810 if (!$this->update_plugin_instance($plugin, $formdata)) {
811 throw new \moodle_exception($plugin->get_error());
812 return false;
815 foreach ($this->feedbackplugins as $plugin) {
816 if (!$this->update_plugin_instance($plugin, $formdata)) {
817 throw new \moodle_exception($plugin->get_error());
818 return false;
822 // In the case of upgrades the coursemodule has not been set,
823 // so we need to wait before calling these two.
824 $this->update_calendar($formdata->coursemodule);
825 if (!empty($formdata->completionexpected)) {
826 \core_completion\api::update_completion_date_event($formdata->coursemodule, 'assign', $this->instance,
827 $formdata->completionexpected);
829 $this->update_gradebook(false, $formdata->coursemodule);
833 $update = new stdClass();
834 $update->id = $this->get_instance()->id;
835 $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
836 $DB->update_record('assign', $update);
838 return $returnid;
842 * Delete all grades from the gradebook for this assignment.
844 * @return bool
846 protected function delete_grades() {
847 global $CFG;
849 $result = grade_update('mod/assign',
850 $this->get_course()->id,
851 'mod',
852 'assign',
853 $this->get_instance()->id,
855 null,
856 array('deleted'=>1));
857 return $result == GRADE_UPDATE_OK;
861 * Delete this instance from the database.
863 * @return bool false if an error occurs
865 public function delete_instance() {
866 global $DB;
867 $result = true;
869 foreach ($this->submissionplugins as $plugin) {
870 if (!$plugin->delete_instance()) {
871 throw new \moodle_exception($plugin->get_error());
872 $result = false;
875 foreach ($this->feedbackplugins as $plugin) {
876 if (!$plugin->delete_instance()) {
877 throw new \moodle_exception($plugin->get_error());
878 $result = false;
882 // Delete files associated with this assignment.
883 $fs = get_file_storage();
884 if (! $fs->delete_area_files($this->context->id) ) {
885 $result = false;
888 $this->delete_all_overrides();
890 // Delete_records will throw an exception if it fails - so no need for error checking here.
891 $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id));
892 $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id));
893 $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id));
894 $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id));
895 $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id));
897 // Delete items from the gradebook.
898 if (! $this->delete_grades()) {
899 $result = false;
902 // Delete the instance.
903 // We must delete the module record after we delete the grade item.
904 $DB->delete_records('assign', array('id'=>$this->get_instance()->id));
906 return $result;
910 * Deletes a assign override from the database and clears any corresponding calendar events
912 * @param int $overrideid The id of the override being deleted
913 * @return bool true on success
915 public function delete_override($overrideid) {
916 global $CFG, $DB;
918 require_once($CFG->dirroot . '/calendar/lib.php');
920 $cm = $this->get_course_module();
921 if (empty($cm)) {
922 $instance = $this->get_instance();
923 $cm = get_coursemodule_from_instance('assign', $instance->id, $instance->course);
926 $override = $DB->get_record('assign_overrides', array('id' => $overrideid), '*', MUST_EXIST);
928 // Delete the events.
929 $conds = array('modulename' => 'assign', 'instance' => $this->get_instance()->id);
930 if (isset($override->userid)) {
931 $conds['userid'] = $override->userid;
932 $cachekey = "{$cm->instance}_u_{$override->userid}";
933 } else {
934 $conds['groupid'] = $override->groupid;
935 $cachekey = "{$cm->instance}_g_{$override->groupid}";
937 $events = $DB->get_records('event', $conds);
938 foreach ($events as $event) {
939 $eventold = calendar_event::load($event);
940 $eventold->delete();
943 $DB->delete_records('assign_overrides', array('id' => $overrideid));
944 cache::make('mod_assign', 'overrides')->delete($cachekey);
946 // Set the common parameters for one of the events we will be triggering.
947 $params = array(
948 'objectid' => $override->id,
949 'context' => context_module::instance($cm->id),
950 'other' => array(
951 'assignid' => $override->assignid
954 // Determine which override deleted event to fire.
955 if (!empty($override->userid)) {
956 $params['relateduserid'] = $override->userid;
957 $event = \mod_assign\event\user_override_deleted::create($params);
958 } else {
959 $params['other']['groupid'] = $override->groupid;
960 $event = \mod_assign\event\group_override_deleted::create($params);
963 // Trigger the override deleted event.
964 $event->add_record_snapshot('assign_overrides', $override);
965 $event->trigger();
967 return true;
971 * Deletes all assign overrides from the database and clears any corresponding calendar events
973 public function delete_all_overrides() {
974 global $DB;
976 $overrides = $DB->get_records('assign_overrides', array('assignid' => $this->get_instance()->id), 'id');
977 foreach ($overrides as $override) {
978 $this->delete_override($override->id);
983 * Updates the assign properties with override information for a user.
985 * Algorithm: For each assign setting, if there is a matching user-specific override,
986 * then use that otherwise, if there are group-specific overrides, return the most
987 * lenient combination of them. If neither applies, leave the assign setting unchanged.
989 * @param int $userid The userid.
991 public function update_effective_access($userid) {
993 $override = $this->override_exists($userid);
995 // Merge with assign defaults.
996 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate', 'timelimit');
997 foreach ($keys as $key) {
998 if (isset($override->{$key})) {
999 $this->get_instance($userid)->{$key} = $override->{$key};
1006 * Returns whether an assign has any overrides.
1008 * @return true if any, false if not
1010 public function has_overrides() {
1011 global $DB;
1013 $override = $DB->record_exists('assign_overrides', array('assignid' => $this->get_instance()->id));
1015 if ($override) {
1016 return true;
1019 return false;
1023 * Returns user override
1025 * Algorithm: For each assign setting, if there is a matching user-specific override,
1026 * then use that otherwise, if there are group-specific overrides, use the one with the
1027 * lowest sort order. If neither applies, leave the assign setting unchanged.
1029 * @param int $userid The userid.
1030 * @return stdClass The override
1032 public function override_exists($userid) {
1033 global $DB;
1035 // Gets an assoc array containing the keys for defined user overrides only.
1036 $getuseroverride = function($userid) use ($DB) {
1037 $useroverride = $DB->get_record('assign_overrides', ['assignid' => $this->get_instance()->id, 'userid' => $userid]);
1038 return $useroverride ? get_object_vars($useroverride) : [];
1041 // Gets an assoc array containing the keys for defined group overrides only.
1042 $getgroupoverride = function($userid) use ($DB) {
1043 $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
1045 if (empty($groupings[0])) {
1046 return [];
1049 // Select all overrides that apply to the User's groups.
1050 list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
1051 $sql = "SELECT * FROM {assign_overrides}
1052 WHERE groupid $extra AND assignid = ? ORDER BY sortorder ASC";
1053 $params[] = $this->get_instance()->id;
1054 $groupoverride = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
1056 return $groupoverride ? get_object_vars($groupoverride) : [];
1059 // Later arguments clobber earlier ones with array_merge. The two helper functions
1060 // return arrays containing keys for only the defined overrides. So we get the
1061 // desired behaviour as per the algorithm.
1062 return (object)array_merge(
1063 ['timelimit' => null, 'duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null],
1064 $getgroupoverride($userid),
1065 $getuseroverride($userid)
1070 * Check if the given calendar_event is either a user or group override
1071 * event.
1073 * @return bool
1075 public function is_override_calendar_event(\calendar_event $event) {
1076 global $DB;
1078 if (!isset($event->modulename)) {
1079 return false;
1082 if ($event->modulename != 'assign') {
1083 return false;
1086 if (!isset($event->instance)) {
1087 return false;
1090 if (!isset($event->userid) && !isset($event->groupid)) {
1091 return false;
1094 $overrideparams = [
1095 'assignid' => $event->instance
1098 if (isset($event->groupid)) {
1099 $overrideparams['groupid'] = $event->groupid;
1100 } else if (isset($event->userid)) {
1101 $overrideparams['userid'] = $event->userid;
1104 if ($DB->get_record('assign_overrides', $overrideparams)) {
1105 return true;
1106 } else {
1107 return false;
1112 * This function calculates the minimum and maximum cutoff values for the timestart of
1113 * the given event.
1115 * It will return an array with two values, the first being the minimum cutoff value and
1116 * the second being the maximum cutoff value. Either or both values can be null, which
1117 * indicates there is no minimum or maximum, respectively.
1119 * If a cutoff is required then the function must return an array containing the cutoff
1120 * timestamp and error string to display to the user if the cutoff value is violated.
1122 * A minimum and maximum cutoff return value will look like:
1124 * [1505704373, 'The due date must be after the sbumission start date'],
1125 * [1506741172, 'The due date must be before the cutoff date']
1128 * If the event does not have a valid timestart range then [false, false] will
1129 * be returned.
1131 * @param calendar_event $event The calendar event to get the time range for
1132 * @return array
1134 function get_valid_calendar_event_timestart_range(\calendar_event $event) {
1135 $instance = $this->get_instance();
1136 $submissionsfromdate = $instance->allowsubmissionsfromdate;
1137 $cutoffdate = $instance->cutoffdate;
1138 $duedate = $instance->duedate;
1139 $gradingduedate = $instance->gradingduedate;
1140 $mindate = null;
1141 $maxdate = null;
1143 if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
1144 // This check is in here because due date events are currently
1145 // the only events that can be overridden, so we can save a DB
1146 // query if we don't bother checking other events.
1147 if ($this->is_override_calendar_event($event)) {
1148 // This is an override event so there is no valid timestart
1149 // range to set it to.
1150 return [false, false];
1153 if ($submissionsfromdate) {
1154 $mindate = [
1155 $submissionsfromdate,
1156 get_string('duedatevalidation', 'assign'),
1160 if ($cutoffdate) {
1161 $maxdate = [
1162 $cutoffdate,
1163 get_string('cutoffdatevalidation', 'assign'),
1167 if ($gradingduedate) {
1168 // If we don't have a cutoff date or we've got a grading due date
1169 // that is earlier than the cutoff then we should use that as the
1170 // upper limit for the due date.
1171 if (!$cutoffdate || $gradingduedate < $cutoffdate) {
1172 $maxdate = [
1173 $gradingduedate,
1174 get_string('gradingdueduedatevalidation', 'assign'),
1178 } else if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) {
1179 if ($duedate) {
1180 $mindate = [
1181 $duedate,
1182 get_string('gradingdueduedatevalidation', 'assign'),
1184 } else if ($submissionsfromdate) {
1185 $mindate = [
1186 $submissionsfromdate,
1187 get_string('gradingduefromdatevalidation', 'assign'),
1192 return [$mindate, $maxdate];
1196 * Actual implementation of the reset course functionality, delete all the
1197 * assignment submissions for course $data->courseid.
1199 * @param stdClass $data the data submitted from the reset course.
1200 * @return array status array
1202 public function reset_userdata($data) {
1203 global $CFG, $DB;
1205 $componentstr = get_string('modulenameplural', 'assign');
1206 $status = [];
1208 $fs = get_file_storage();
1209 if (!empty($data->reset_assign_submissions)) {
1210 // Delete files associated with this assignment.
1211 foreach ($this->submissionplugins as $plugin) {
1212 $fileareas = [];
1213 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1214 $fileareas = $plugin->get_file_areas();
1215 foreach ($fileareas as $filearea => $notused) {
1216 $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1219 if (!$plugin->delete_instance()) {
1220 $status[] = [
1221 'component' => $componentstr,
1222 'item' => get_string('deleteallsubmissions', 'assign'),
1223 'error' => $plugin->get_error(),
1228 foreach ($this->feedbackplugins as $plugin) {
1229 $fileareas = array();
1230 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1231 $fileareas = $plugin->get_file_areas();
1232 foreach ($fileareas as $filearea => $notused) {
1233 $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1236 if (!$plugin->delete_instance()) {
1237 $status[] = [
1238 'component' => $componentstr,
1239 'item ' => get_string('deleteallsubmissions', 'assign'),
1240 'error' => $plugin->get_error(),
1245 $assignids = $DB->get_records('assign', ['course' => $data->courseid], '', 'id');
1246 list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids));
1248 $DB->delete_records_select('assign_submission', "assignment $sql", $params);
1249 $DB->delete_records_select('assign_user_flags', "assignment $sql", $params);
1251 $status[] = [
1252 'component' => $componentstr,
1253 'item' => get_string('deleteallsubmissions', 'assign'),
1254 'error' => false,
1257 if (!empty($data->reset_gradebook_grades)) {
1258 $DB->delete_records_select('assign_grades', "assignment $sql", $params);
1259 // Remove all grades from gradebook.
1260 require_once($CFG->dirroot . '/mod/assign/lib.php');
1261 assign_reset_gradebook($data->courseid);
1264 // Reset revealidentities for assign if blindmarking is enabled.
1265 if ($this->get_instance()->blindmarking) {
1266 $DB->set_field('assign', 'revealidentities', 0, ['id' => $this->get_instance()->id]);
1270 $purgeoverrides = false;
1272 // Remove user overrides.
1273 if (!empty($data->reset_assign_user_overrides)) {
1274 $DB->delete_records_select('assign_overrides',
1275 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND userid IS NOT NULL', [$data->courseid]);
1276 $status[] = [
1277 'component' => $componentstr,
1278 'item' => get_string('useroverrides', 'assign'),
1279 'error' => false,
1281 $purgeoverrides = true;
1283 // Remove group overrides.
1284 if (!empty($data->reset_assign_group_overrides)) {
1285 $DB->delete_records_select('assign_overrides',
1286 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND groupid IS NOT NULL', [$data->courseid]);
1287 $status[] = [
1288 'component' => $componentstr,
1289 'item' => get_string('groupoverrides', 'assign'),
1290 'error' => false,
1292 $purgeoverrides = true;
1295 // Updating dates - shift may be negative too.
1296 if ($data->timeshift) {
1297 $sql = "UPDATE {assign_overrides}
1298 SET allowsubmissionsfromdate = allowsubmissionsfromdate + ?
1299 WHERE assignid = ? AND allowsubmissionsfromdate <> 0";
1300 $DB->execute(
1301 $sql,
1302 [$data->timeshift, $this->get_instance()->id],
1304 $sql = "UPDATE {assign_overrides}
1305 SET duedate = duedate + ?
1306 WHERE assignid = ? AND duedate <> 0";
1307 $DB->execute(
1308 $sql,
1309 [$data->timeshift, $this->get_instance()->id],
1311 $sql = "UPDATE {assign_overrides}
1312 SET cutoffdate = cutoffdate + ?
1313 WHERE assignid =? AND cutoffdate <> 0";
1314 $DB->execute(
1315 $sql,
1316 [$data->timeshift, $this->get_instance()->id],
1319 $purgeoverrides = true;
1321 // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
1322 // See MDL-9367.
1323 shift_course_mod_dates(
1324 'assign',
1325 ['duedate', 'allowsubmissionsfromdate', 'cutoffdate'],
1326 $data->timeshift,
1327 $data->courseid, $this->get_instance()->id,
1329 $status[] = [
1330 'component' => $componentstr,
1331 'item' => get_string('date'),
1332 'error' => false,
1336 if ($purgeoverrides) {
1337 cache::make('mod_assign', 'overrides')->purge();
1340 return $status;
1344 * Update the settings for a single plugin.
1346 * @param assign_plugin $plugin The plugin to update
1347 * @param stdClass $formdata The form data
1348 * @return bool false if an error occurs
1350 protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) {
1351 if ($plugin->is_visible()) {
1352 $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1353 if (!empty($formdata->$enabledname)) {
1354 $plugin->enable();
1355 if (!$plugin->save_settings($formdata)) {
1356 throw new \moodle_exception($plugin->get_error());
1357 return false;
1359 } else {
1360 $plugin->disable();
1363 return true;
1367 * Update the gradebook information for this assignment.
1369 * @param bool $reset If true, will reset all grades in the gradbook for this assignment
1370 * @param int $coursemoduleid This is required because it might not exist in the database yet
1371 * @return bool
1373 public function update_gradebook($reset, $coursemoduleid) {
1374 global $CFG;
1376 require_once($CFG->dirroot.'/mod/assign/lib.php');
1377 $assign = clone $this->get_instance();
1378 $assign->cmidnumber = $coursemoduleid;
1380 // Set assign gradebook feedback plugin status (enabled and visible).
1381 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
1383 $param = null;
1384 if ($reset) {
1385 $param = 'reset';
1388 return assign_grade_item_update($assign, $param);
1392 * Get the marking table page size
1394 * @return integer
1396 public function get_assign_perpage() {
1397 $perpage = (int) get_user_preferences('assign_perpage', 10);
1398 $adminconfig = $this->get_admin_config();
1399 $maxperpage = -1;
1400 if (isset($adminconfig->maxperpage)) {
1401 $maxperpage = $adminconfig->maxperpage;
1403 if (isset($maxperpage) &&
1404 $maxperpage != -1 &&
1405 ($perpage == -1 || $perpage > $maxperpage)) {
1406 $perpage = $maxperpage;
1408 return $perpage;
1412 * Load and cache the admin config for this module.
1414 * @return stdClass the plugin config
1416 public function get_admin_config() {
1417 if ($this->adminconfig) {
1418 return $this->adminconfig;
1420 $this->adminconfig = get_config('assign');
1421 return $this->adminconfig;
1425 * Update the calendar entries for this assignment.
1427 * @param int $coursemoduleid - Required to pass this in because it might
1428 * not exist in the database yet.
1429 * @return bool
1431 public function update_calendar($coursemoduleid) {
1432 global $DB, $CFG;
1433 require_once($CFG->dirroot.'/calendar/lib.php');
1435 // Special case for add_instance as the coursemodule has not been set yet.
1436 $instance = $this->get_instance();
1438 // Start with creating the event.
1439 $event = new stdClass();
1440 $event->modulename = 'assign';
1441 $event->courseid = $instance->course;
1442 $event->groupid = 0;
1443 $event->userid = 0;
1444 $event->instance = $instance->id;
1445 $event->type = CALENDAR_EVENT_TYPE_ACTION;
1447 // Convert the links to pluginfile. It is a bit hacky but at this stage the files
1448 // might not have been saved in the module area yet.
1449 $intro = $instance->intro;
1450 if ($draftid = file_get_submitted_draft_itemid('introeditor')) {
1451 $intro = file_rewrite_urls_to_pluginfile($intro, $draftid);
1454 // We need to remove the links to files as the calendar is not ready
1455 // to support module events with file areas.
1456 $intro = strip_pluginfile_content($intro);
1457 if ($this->show_intro()) {
1458 $event->description = array(
1459 'text' => $intro,
1460 'format' => $instance->introformat
1462 } else {
1463 $event->description = array(
1464 'text' => '',
1465 'format' => $instance->introformat
1469 $eventtype = ASSIGN_EVENT_TYPE_DUE;
1470 if ($instance->duedate) {
1471 $event->name = get_string('calendardue', 'assign', $instance->name);
1472 $event->eventtype = $eventtype;
1473 $event->timestart = $instance->duedate;
1474 $event->timesort = $instance->duedate;
1475 $select = "modulename = :modulename
1476 AND instance = :instance
1477 AND eventtype = :eventtype
1478 AND groupid = 0
1479 AND courseid <> 0";
1480 $params = array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype);
1481 $event->id = $DB->get_field_select('event', 'id', $select, $params);
1483 // Now process the event.
1484 if ($event->id) {
1485 $calendarevent = calendar_event::load($event->id);
1486 $calendarevent->update($event, false);
1487 } else {
1488 calendar_event::create($event, false);
1490 } else {
1491 $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1492 'eventtype' => $eventtype));
1495 $eventtype = ASSIGN_EVENT_TYPE_GRADINGDUE;
1496 if ($instance->gradingduedate) {
1497 $event->name = get_string('calendargradingdue', 'assign', $instance->name);
1498 $event->eventtype = $eventtype;
1499 $event->timestart = $instance->gradingduedate;
1500 $event->timesort = $instance->gradingduedate;
1501 $event->id = $DB->get_field('event', 'id', array('modulename' => 'assign',
1502 'instance' => $instance->id, 'eventtype' => $event->eventtype));
1504 // Now process the event.
1505 if ($event->id) {
1506 $calendarevent = calendar_event::load($event->id);
1507 $calendarevent->update($event, false);
1508 } else {
1509 calendar_event::create($event, false);
1511 } else {
1512 $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1513 'eventtype' => $eventtype));
1516 return true;
1520 * Update this instance in the database.
1522 * @param stdClass $formdata - the data submitted from the form
1523 * @return bool false if an error occurs
1525 public function update_instance($formdata) {
1526 global $DB;
1527 $adminconfig = $this->get_admin_config();
1529 $update = new stdClass();
1530 $update->id = $formdata->instance;
1531 $update->name = $formdata->name;
1532 $update->timemodified = time();
1533 $update->course = $formdata->course;
1534 $update->intro = $formdata->intro;
1535 $update->introformat = $formdata->introformat;
1536 $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
1537 if (isset($formdata->activityeditor)) {
1538 $update->activity = $this->save_editor_draft_files($formdata);
1539 $update->activityformat = $formdata->activityeditor['format'];
1541 if (isset($formdata->submissionattachments)) {
1542 $update->submissionattachments = $formdata->submissionattachments;
1544 $update->submissiondrafts = $formdata->submissiondrafts;
1545 $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
1546 $update->sendnotifications = $formdata->sendnotifications;
1547 $update->sendlatenotifications = $formdata->sendlatenotifications;
1548 $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
1549 if (isset($formdata->sendstudentnotifications)) {
1550 $update->sendstudentnotifications = $formdata->sendstudentnotifications;
1552 $update->duedate = $formdata->duedate;
1553 $update->cutoffdate = $formdata->cutoffdate;
1554 if (isset($formdata->timelimit)) {
1555 $update->timelimit = $formdata->timelimit;
1557 $update->gradingduedate = $formdata->gradingduedate;
1558 $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
1559 $update->grade = $formdata->grade;
1560 if (!empty($formdata->completionunlocked)) {
1561 $update->completionsubmit = !empty($formdata->completionsubmit);
1563 $update->teamsubmission = $formdata->teamsubmission;
1564 $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
1565 if (isset($formdata->teamsubmissiongroupingid)) {
1566 $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
1568 if (isset($formdata->hidegrader)) {
1569 $update->hidegrader = $formdata->hidegrader;
1571 $update->blindmarking = $formdata->blindmarking;
1572 $update->maxattempts = $formdata->maxattempts ?? 1;
1573 $update->attemptreopenmethod = $formdata->attemptreopenmethod ?? ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS;
1574 if (isset($formdata->preventsubmissionnotingroup)) {
1575 $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
1577 $update->markingworkflow = $formdata->markingworkflow;
1578 $update->markingallocation = $formdata->markingallocation;
1579 if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
1580 $update->markingallocation = 0;
1582 $update->markinganonymous = $formdata->markinganonymous;
1583 // If marking workflow is disabled, or blindmarking is disabled then make sure marking anonymous is disabled.
1584 if (empty($update->markingworkflow) || empty($update->blindmarking)) {
1585 $update->markinganonymous = 0;
1588 $result = $DB->update_record('assign', $update);
1589 $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
1591 $this->save_intro_draft_files($formdata);
1593 // Load the assignment so the plugins have access to it.
1595 // Call save_settings hook for submission plugins.
1596 foreach ($this->submissionplugins as $plugin) {
1597 if (!$this->update_plugin_instance($plugin, $formdata)) {
1598 throw new \moodle_exception($plugin->get_error());
1599 return false;
1602 foreach ($this->feedbackplugins as $plugin) {
1603 if (!$this->update_plugin_instance($plugin, $formdata)) {
1604 throw new \moodle_exception($plugin->get_error());
1605 return false;
1609 $this->update_calendar($this->get_course_module()->id);
1610 $completionexpected = (!empty($formdata->completionexpected)) ? $formdata->completionexpected : null;
1611 \core_completion\api::update_completion_date_event($this->get_course_module()->id, 'assign', $this->instance,
1612 $completionexpected);
1613 $this->update_gradebook(false, $this->get_course_module()->id);
1615 $update = new stdClass();
1616 $update->id = $this->get_instance()->id;
1617 $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
1618 $DB->update_record('assign', $update);
1620 return $result;
1624 * Save the attachments in the intro description.
1626 * @param stdClass $formdata
1628 protected function save_intro_draft_files($formdata) {
1629 if (isset($formdata->introattachments)) {
1630 file_save_draft_area_files($formdata->introattachments, $this->get_context()->id,
1631 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
1636 * Save the attachments in the editor description.
1638 * @param stdClass $formdata
1640 protected function save_editor_draft_files($formdata): string {
1641 $text = '';
1642 if (isset($formdata->activityeditor)) {
1643 $text = $formdata->activityeditor['text'];
1644 if (isset($formdata->activityeditor['itemid'])) {
1645 $text = file_save_draft_area_files($formdata->activityeditor['itemid'], $this->get_context()->id,
1646 'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA,
1647 0, array('subdirs' => true), $formdata->activityeditor['text']);
1650 return $text;
1655 * Add elements in grading plugin form.
1657 * @param mixed $grade stdClass|null
1658 * @param MoodleQuickForm $mform
1659 * @param stdClass $data
1660 * @param int $userid - The userid we are grading
1661 * @return void
1663 protected function add_plugin_grade_elements($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
1664 foreach ($this->feedbackplugins as $plugin) {
1665 if ($plugin->is_enabled() && $plugin->is_visible()) {
1666 $plugin->get_form_elements_for_user($grade, $mform, $data, $userid);
1674 * Add one plugins settings to edit plugin form.
1676 * @param assign_plugin $plugin The plugin to add the settings from
1677 * @param MoodleQuickForm $mform The form to add the configuration settings to.
1678 * This form is modified directly (not returned).
1679 * @param array $pluginsenabled A list of form elements to be added to a group.
1680 * The new element is added to this array by this function.
1681 * @return void
1683 protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) {
1684 global $CFG;
1685 if ($plugin->is_visible() && !$plugin->is_configurable() && $plugin->is_enabled()) {
1686 $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1687 $pluginsenabled[] = $mform->createElement('hidden', $name, 1);
1688 $mform->setType($name, PARAM_BOOL);
1689 $plugin->get_settings($mform);
1690 } else if ($plugin->is_visible() && $plugin->is_configurable()) {
1691 $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1692 $label = $plugin->get_name();
1693 $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label);
1694 $helpicon = $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type());
1695 $pluginsenabled[] = $mform->createElement('static', '', '', $helpicon);
1697 $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default');
1698 if ($plugin->get_config('enabled') !== false) {
1699 $default = $plugin->is_enabled();
1701 $mform->setDefault($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled', $default);
1703 $plugin->get_settings($mform);
1709 * Add settings to edit plugin form.
1711 * @param MoodleQuickForm $mform The form to add the configuration settings to.
1712 * This form is modified directly (not returned).
1713 * @return void
1715 public function add_all_plugin_settings(MoodleQuickForm $mform) {
1716 $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign'));
1718 $submissionpluginsenabled = array();
1719 $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false);
1720 foreach ($this->submissionplugins as $plugin) {
1721 $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled);
1723 $group->setElements($submissionpluginsenabled);
1725 $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign'));
1726 $feedbackpluginsenabled = array();
1727 $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false);
1728 foreach ($this->feedbackplugins as $plugin) {
1729 $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled);
1731 $group->setElements($feedbackpluginsenabled);
1732 $mform->setExpanded('submissiontypes');
1736 * Allow each plugin an opportunity to update the defaultvalues
1737 * passed in to the settings form (needed to set up draft areas for
1738 * editor and filemanager elements)
1740 * @param array $defaultvalues
1742 public function plugin_data_preprocessing(&$defaultvalues) {
1743 foreach ($this->submissionplugins as $plugin) {
1744 if ($plugin->is_visible()) {
1745 $plugin->data_preprocessing($defaultvalues);
1748 foreach ($this->feedbackplugins as $plugin) {
1749 if ($plugin->is_visible()) {
1750 $plugin->data_preprocessing($defaultvalues);
1756 * Get the name of the current module.
1758 * @return string the module name (Assignment)
1760 protected function get_module_name() {
1761 if (isset(self::$modulename)) {
1762 return self::$modulename;
1764 self::$modulename = get_string('modulename', 'assign');
1765 return self::$modulename;
1769 * Get the plural name of the current module.
1771 * @return string the module name plural (Assignments)
1773 protected function get_module_name_plural() {
1774 if (isset(self::$modulenameplural)) {
1775 return self::$modulenameplural;
1777 self::$modulenameplural = get_string('modulenameplural', 'assign');
1778 return self::$modulenameplural;
1782 * Has this assignment been constructed from an instance?
1784 * @return bool
1786 public function has_instance() {
1787 return $this->instance || $this->get_course_module();
1791 * Get the settings for the current instance of this assignment.
1793 * @return stdClass The settings
1794 * @throws dml_exception
1796 public function get_default_instance() {
1797 global $DB;
1798 if (!$this->instance && $this->get_course_module()) {
1799 $params = array('id' => $this->get_course_module()->instance);
1800 $this->instance = $DB->get_record('assign', $params, '*', MUST_EXIST);
1802 $this->userinstances = [];
1804 return $this->instance;
1808 * Get the settings for the current instance of this assignment
1809 * @param int|null $userid the id of the user to load the assign instance for.
1810 * @return stdClass The settings
1812 public function get_instance(?int $userid = null): stdClass {
1813 global $USER;
1814 $userid = $userid ?? $USER->id;
1816 $this->instance = $this->get_default_instance();
1818 // If we have the user instance already, just return it.
1819 if (isset($this->userinstances[$userid])) {
1820 return $this->userinstances[$userid];
1823 // Calculate properties which vary per user.
1824 $this->userinstances[$userid] = $this->calculate_properties($this->instance, $userid);
1825 return $this->userinstances[$userid];
1829 * Calculates and updates various properties based on the specified user.
1831 * @param stdClass $record the raw assign record.
1832 * @param int $userid the id of the user to calculate the properties for.
1833 * @return stdClass a new record having calculated properties.
1835 private function calculate_properties(\stdClass $record, int $userid): \stdClass {
1836 $record = clone ($record);
1838 // Relative dates.
1839 if (!empty($record->duedate)) {
1840 $course = $this->get_course();
1841 $usercoursedates = course_get_course_dates_for_user_id($course, $userid);
1842 if ($usercoursedates['start']) {
1843 $userprops = ['duedate' => $record->duedate + $usercoursedates['startoffset']];
1844 $record = (object) array_merge((array) $record, (array) $userprops);
1847 return $record;
1851 * Get the primary grade item for this assign instance.
1853 * @return grade_item The grade_item record
1855 public function get_grade_item() {
1856 if ($this->gradeitem) {
1857 return $this->gradeitem;
1859 $instance = $this->get_instance();
1860 $params = array('itemtype' => 'mod',
1861 'itemmodule' => 'assign',
1862 'iteminstance' => $instance->id,
1863 'courseid' => $instance->course,
1864 'itemnumber' => 0);
1865 $this->gradeitem = grade_item::fetch($params);
1866 if (!$this->gradeitem) {
1867 throw new coding_exception('Improper use of the assignment class. ' .
1868 'Cannot load the grade item.');
1870 return $this->gradeitem;
1874 * Get the context of the current course.
1876 * @return mixed context|null The course context
1878 public function get_course_context() {
1879 if (!$this->context && !$this->course) {
1880 throw new coding_exception('Improper use of the assignment class. ' .
1881 'Cannot load the course context.');
1883 if ($this->context) {
1884 return $this->context->get_course_context();
1885 } else {
1886 return context_course::instance($this->course->id);
1892 * Get the current course module.
1894 * @return cm_info|null The course module or null if not known
1896 public function get_course_module() {
1897 if ($this->coursemodule) {
1898 return $this->coursemodule;
1900 if (!$this->context) {
1901 return null;
1904 if ($this->context->contextlevel == CONTEXT_MODULE) {
1905 $modinfo = get_fast_modinfo($this->get_course());
1906 $this->coursemodule = $modinfo->get_cm($this->context->instanceid);
1907 return $this->coursemodule;
1909 return null;
1913 * Get context module.
1915 * @return context
1917 public function get_context() {
1918 return $this->context;
1922 * Get the current course.
1924 * @return mixed stdClass|null The course
1926 public function get_course() {
1927 global $DB;
1929 if ($this->course && is_object($this->course)) {
1930 return $this->course;
1933 if (!$this->context) {
1934 return null;
1936 $params = array('id' => $this->get_course_context()->instanceid);
1937 $this->course = $DB->get_record('course', $params, '*', MUST_EXIST);
1939 return $this->course;
1943 * Count the number of intro attachments.
1945 * @return int
1947 protected function count_attachments() {
1949 $fs = get_file_storage();
1950 $files = $fs->get_area_files($this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA,
1951 0, 'id', false);
1953 return count($files);
1957 * Are there any intro attachments to display?
1959 * @return boolean
1961 protected function has_visible_attachments() {
1962 return ($this->count_attachments() > 0);
1966 * Check if the intro attachments should be provided to the user.
1968 * @param int $userid User id.
1969 * @return bool
1971 public function should_provide_intro_attachments(int $userid): bool {
1972 $instance = $this->get_instance($userid);
1974 // Check if user has permission to view attachments regardless of assignment settings.
1975 if (has_capability('moodle/course:manageactivities', $this->get_context())) {
1976 return true;
1979 // If assignment does not show intro, we never provide intro attachments.
1980 if (!$this->show_intro()) {
1981 return false;
1984 // If intro attachments should only be shown when submission is started, check if there is an open submission.
1985 if (!empty($instance->submissionattachments) && !$this->submissions_open($userid, true)) {
1986 return false;
1989 return true;
1993 * Return a grade in user-friendly form, whether it's a scale or not.
1995 * @param mixed $grade int|null
1996 * @param boolean $editing Are we allowing changes to this grade?
1997 * @param int $userid The user id the grade belongs to
1998 * @param int $modified Timestamp from when the grade was last modified
1999 * @return string User-friendly representation of grade
2001 public function display_grade($grade, $editing, $userid=0, $modified=0) {
2002 global $DB;
2004 static $scalegrades = array();
2006 $o = '';
2008 if ($this->get_instance()->grade >= 0) {
2009 // Normal number.
2010 if ($editing && $this->get_instance()->grade > 0) {
2011 if ($grade < 0) {
2012 $displaygrade = '';
2013 } else {
2014 $displaygrade = format_float($grade, $this->get_grade_item()->get_decimals());
2016 $o .= '<label class="accesshide" for="quickgrade_' . $userid . '">' .
2017 get_string('usergrade', 'assign') .
2018 '</label>';
2019 $o .= '<input type="text"
2020 id="quickgrade_' . $userid . '"
2021 name="quickgrade_' . $userid . '"
2022 value="' . $displaygrade . '"
2023 size="6"
2024 maxlength="10"
2025 class="quickgrade"/>';
2026 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $this->get_grade_item()->get_decimals());
2027 return $o;
2028 } else {
2029 if ($grade == -1 || $grade === null) {
2030 $o .= '-';
2031 } else {
2032 $item = $this->get_grade_item();
2033 $o .= grade_format_gradevalue($grade, $item);
2034 if ($item->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
2035 // If displaying the raw grade, also display the total value.
2036 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $item->get_decimals());
2039 return $o;
2042 } else {
2043 // Scale.
2044 if (empty($this->cache['scale'])) {
2045 if ($scale = $DB->get_record('scale', array('id'=>-($this->get_instance()->grade)))) {
2046 $this->cache['scale'] = make_menu_from_list($scale->scale);
2047 } else {
2048 $o .= '-';
2049 return $o;
2052 if ($editing) {
2053 $o .= '<label class="accesshide"
2054 for="quickgrade_' . $userid . '">' .
2055 get_string('usergrade', 'assign') .
2056 '</label>';
2057 $o .= '<select name="quickgrade_' . $userid . '" class="quickgrade">';
2058 $o .= '<option value="-1">' . get_string('nograde') . '</option>';
2059 foreach ($this->cache['scale'] as $optionid => $option) {
2060 $selected = '';
2061 if ($grade == $optionid) {
2062 $selected = 'selected="selected"';
2064 $o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>';
2066 $o .= '</select>';
2067 return $o;
2068 } else {
2069 $scaleid = (int)$grade;
2070 if (isset($this->cache['scale'][$scaleid])) {
2071 $o .= $this->cache['scale'][$scaleid];
2072 return $o;
2074 $o .= '-';
2075 return $o;
2081 * Get the submission status/grading status for all submissions in this assignment for the
2082 * given paticipants.
2084 * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
2085 * If this is a group assignment, group info is also returned.
2087 * @param array $participants an associative array where the key is the participant id and
2088 * the value is the participant record.
2089 * @return array an associative array where the key is the participant id and the value is
2090 * the participant record.
2092 private function get_submission_info_for_participants($participants) {
2093 global $DB;
2095 if (empty($participants)) {
2096 return $participants;
2099 list($insql, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
2101 $assignid = $this->get_instance()->id;
2102 $params['assignmentid1'] = $assignid;
2103 $params['assignmentid2'] = $assignid;
2104 $params['assignmentid3'] = $assignid;
2106 $fields = 'SELECT u.id, s.status, s.timemodified AS stime, g.timemodified AS gtime, g.grade, uf.extensionduedate';
2107 $from = ' FROM {user} u
2108 LEFT JOIN {assign_submission} s
2109 ON u.id = s.userid
2110 AND s.assignment = :assignmentid1
2111 AND s.latest = 1
2112 LEFT JOIN {assign_grades} g
2113 ON u.id = g.userid
2114 AND g.assignment = :assignmentid2
2115 AND g.attemptnumber = s.attemptnumber
2116 LEFT JOIN {assign_user_flags} uf
2117 ON u.id = uf.userid
2118 AND uf.assignment = :assignmentid3
2120 $where = ' WHERE u.id ' . $insql;
2122 if (!empty($this->get_instance()->blindmarking)) {
2123 $from .= 'LEFT JOIN {assign_user_mapping} um
2124 ON u.id = um.userid
2125 AND um.assignment = :assignmentid4 ';
2126 $params['assignmentid4'] = $assignid;
2127 $fields .= ', um.id as recordid ';
2130 $sql = "$fields $from $where";
2132 $records = $DB->get_records_sql($sql, $params);
2134 if ($this->get_instance()->teamsubmission) {
2135 // Get all groups.
2136 $allgroups = groups_get_all_groups($this->get_course()->id,
2137 array_keys($participants),
2138 $this->get_instance()->teamsubmissiongroupingid,
2139 'DISTINCT g.id, g.name');
2142 foreach ($participants as $userid => $participant) {
2143 $participants[$userid]->fullname = $this->fullname($participant);
2144 $participants[$userid]->submitted = false;
2145 $participants[$userid]->requiregrading = false;
2146 $participants[$userid]->grantedextension = false;
2147 $participants[$userid]->submissionstatus = '';
2150 foreach ($records as $userid => $submissioninfo) {
2151 // These filters are 100% the same as the ones in the grading table SQL.
2152 $submitted = false;
2153 $requiregrading = false;
2154 $grantedextension = false;
2155 $submissionstatus = !empty($submissioninfo->status) ? $submissioninfo->status : '';
2157 if (!empty($submissioninfo->stime) && $submissioninfo->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
2158 $submitted = true;
2161 if ($submitted && ($submissioninfo->stime >= $submissioninfo->gtime ||
2162 empty($submissioninfo->gtime) ||
2163 $submissioninfo->grade === null)) {
2164 $requiregrading = true;
2167 if (!empty($submissioninfo->extensionduedate)) {
2168 $grantedextension = true;
2171 $participants[$userid]->submitted = $submitted;
2172 $participants[$userid]->requiregrading = $requiregrading;
2173 $participants[$userid]->grantedextension = $grantedextension;
2174 $participants[$userid]->submissionstatus = $submissionstatus;
2175 if ($this->get_instance()->teamsubmission) {
2176 $group = $this->get_submission_group($userid);
2177 if ($group) {
2178 $participants[$userid]->groupid = $group->id;
2179 $participants[$userid]->groupname = $group->name;
2183 return $participants;
2187 * Get the submission status/grading status for all submissions in this assignment.
2188 * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
2189 * If this is a group assignment, group info is also returned.
2191 * @param int $currentgroup
2192 * @param boolean $tablesort Apply current user table sorting preferences.
2193 * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'grantedextension',
2194 * 'groupid', 'groupname'
2196 public function list_participants_with_filter_status_and_group($currentgroup, $tablesort = false) {
2197 $participants = $this->list_participants($currentgroup, false, $tablesort);
2199 if (empty($participants)) {
2200 return $participants;
2201 } else {
2202 return $this->get_submission_info_for_participants($participants);
2207 * Return a valid order by segment for list_participants that matches
2208 * the sorting of the current grading table. Not every field is supported,
2209 * we are only concerned with a list of users so we can't search on anything
2210 * that is not part of the user information (like grading statud or last modified stuff).
2212 * @return string Order by clause for list_participants
2214 private function get_grading_sort_sql() {
2215 $usersort = flexible_table::get_sort_for_table('mod_assign_grading');
2216 // TODO Does not support custom user profile fields (MDL-70456).
2217 $userfieldsapi = \core_user\fields::for_identity($this->context, false)->with_userpic();
2218 $userfields = $userfieldsapi->get_required_fields();
2219 $orderfields = explode(',', $usersort);
2220 $validlist = [];
2222 foreach ($orderfields as $orderfield) {
2223 $orderfield = trim($orderfield);
2224 foreach ($userfields as $field) {
2225 $parts = explode(' ', $orderfield);
2226 if ($parts[0] == $field) {
2227 // Prepend the user table prefix and count this as a valid order field.
2228 array_push($validlist, 'u.' . $orderfield);
2232 // Produce a final list.
2233 $result = implode(',', $validlist);
2234 if (empty($result)) {
2235 // Fall back ordering when none has been set.
2236 $result = 'u.lastname, u.firstname, u.id';
2239 return $result;
2243 * Returns array with sql code and parameters returning all ids of users who have submitted an assignment.
2245 * @param int $group The group that the query is for.
2246 * @return array list($sql, $params)
2248 protected function get_submitted_sql($group = 0) {
2249 // We need to guarentee unique table names.
2250 static $i = 0;
2251 $i++;
2252 $prefix = 'sa' . $i . '_';
2253 $params = [
2254 "{$prefix}assignment" => (int) $this->get_instance()->id,
2255 "{$prefix}status" => ASSIGN_SUBMISSION_STATUS_NEW,
2257 $capjoin = get_enrolled_with_capabilities_join($this->context, $prefix, '', $group, $this->show_only_active_users());
2258 $params += $capjoin->params;
2259 $sql = "SELECT {$prefix}s.userid
2260 FROM {assign_submission} {$prefix}s
2261 JOIN {user} {$prefix}u ON {$prefix}u.id = {$prefix}s.userid
2262 $capjoin->joins
2263 WHERE {$prefix}s.assignment = :{$prefix}assignment
2264 AND {$prefix}s.status <> :{$prefix}status
2265 AND $capjoin->wheres";
2266 return array($sql, $params);
2270 * Load a list of users enrolled in the current course with the specified permission and group.
2271 * 0 for no group.
2272 * Apply any current sort filters from the grading table.
2274 * @param int $currentgroup
2275 * @param bool $idsonly
2276 * @param bool $tablesort
2277 * @return array List of user records
2279 public function list_participants($currentgroup, $idsonly, $tablesort = false) {
2280 global $DB, $USER;
2282 // Get the last known sort order for the grading table.
2284 if (empty($currentgroup)) {
2285 $currentgroup = 0;
2288 $key = $this->context->id . '-' . $currentgroup . '-' . $this->show_only_active_users();
2289 if (!isset($this->participants[$key])) {
2290 list($esql, $params) = get_enrolled_sql($this->context, 'mod/assign:submit', $currentgroup,
2291 $this->show_only_active_users());
2292 list($ssql, $sparams) = $this->get_submitted_sql($currentgroup);
2293 $params += $sparams;
2295 $fields = 'u.*';
2296 $orderby = 'u.lastname, u.firstname, u.id';
2298 $additionaljoins = '';
2299 $additionalfilters = '';
2300 $instance = $this->get_instance();
2301 if (!empty($instance->blindmarking)) {
2302 $additionaljoins .= " LEFT JOIN {assign_user_mapping} um
2303 ON u.id = um.userid
2304 AND um.assignment = :assignmentid1
2305 LEFT JOIN {assign_submission} s
2306 ON u.id = s.userid
2307 AND s.assignment = :assignmentid2
2308 AND s.latest = 1
2310 $params['assignmentid1'] = (int) $instance->id;
2311 $params['assignmentid2'] = (int) $instance->id;
2312 $fields .= ', um.id as recordid ';
2314 // Sort by submission time first, then by um.id to sort reliably by the blind marking id.
2315 // Note, different DBs have different ordering of NULL values.
2316 // Therefore we coalesce the current time into the timecreated field, and the max possible integer into
2317 // the ID field.
2318 if (empty($tablesort)) {
2319 $orderby = "COALESCE(s.timecreated, " . time() . ") ASC, COALESCE(s.id, " . PHP_INT_MAX . ") ASC, um.id ASC";
2323 if ($instance->markingworkflow &&
2324 $instance->markingallocation &&
2325 !has_capability('mod/assign:manageallocations', $this->get_context()) &&
2326 has_capability('mod/assign:grade', $this->get_context())) {
2328 $additionaljoins .= ' LEFT JOIN {assign_user_flags} uf
2329 ON u.id = uf.userid
2330 AND uf.assignment = :assignmentid3';
2332 $params['assignmentid3'] = (int) $instance->id;
2334 $additionalfilters .= ' AND uf.allocatedmarker = :markerid';
2335 $params['markerid'] = $USER->id;
2338 // A user wants to view a particular user rather than a set of users.
2339 if ($this->usersearch) {
2340 if (isset($this->usersearch['userid'])) {
2341 $additionalfilters .= " AND u.id = :uid";
2342 $params['uid'] = $this->usersearch['userid'];
2343 } else if ($this->usersearch['usersearch'] !== '') { // A user wants to view a subset of learners that match the search criteria.
2345 'where' => $keywordswhere,
2346 'params' => $keywordsparams,
2347 ] = \core_user::get_users_search_sql($this->context, $this->usersearch['usersearch']);
2348 $additionalfilters .= " AND $keywordswhere";
2349 $params = array_merge($params, $keywordsparams);
2353 $sql = "SELECT $fields
2354 FROM {user} u
2355 JOIN ($esql UNION $ssql) je ON je.id = u.id
2356 $additionaljoins
2357 WHERE u.deleted = 0
2358 $additionalfilters
2359 ORDER BY $orderby";
2361 $users = $DB->get_records_sql($sql, $params);
2363 $cm = $this->get_course_module();
2364 $info = new \core_availability\info_module($cm);
2365 $users = $info->filter_user_list($users);
2367 $this->participants[$key] = $users;
2370 if ($tablesort) {
2371 // Resort the user list according to the grading table sort and filter settings.
2372 $sortedfiltereduserids = $this->get_grading_userid_list(true, '');
2373 $sortedfilteredusers = [];
2374 foreach ($sortedfiltereduserids as $nextid) {
2375 $nextid = intval($nextid);
2376 if (isset($this->participants[$key][$nextid])) {
2377 $sortedfilteredusers[$nextid] = $this->participants[$key][$nextid];
2380 $this->participants[$key] = $sortedfilteredusers;
2383 if ($idsonly) {
2384 $idslist = array();
2385 foreach ($this->participants[$key] as $id => $user) {
2386 $idslist[$id] = new stdClass();
2387 $idslist[$id]->id = $id;
2389 return $idslist;
2391 return $this->participants[$key];
2395 * Load a user if they are enrolled in the current course. Populated with submission
2396 * status for this assignment.
2398 * @param int $userid
2399 * @return null|stdClass user record
2401 public function get_participant($userid) {
2402 global $DB, $USER;
2404 if ($userid == $USER->id) {
2405 $participant = clone ($USER);
2406 } else {
2407 $participant = $DB->get_record('user', array('id' => $userid));
2409 if (!$participant) {
2410 return null;
2413 if (!is_enrolled($this->context, $participant, '', $this->show_only_active_users())) {
2414 return null;
2417 $result = $this->get_submission_info_for_participants(array($participant->id => $participant));
2419 $submissioninfo = $result[$participant->id];
2420 if (!$submissioninfo->submitted && !has_capability('mod/assign:submit', $this->context, $userid)) {
2421 return null;
2424 return $submissioninfo;
2428 * Load a count of valid teams for this assignment.
2430 * @param int $activitygroup Activity active group
2431 * @return int number of valid teams
2433 public function count_teams($activitygroup = 0) {
2435 $count = 0;
2437 $participants = $this->list_participants($activitygroup, true);
2439 // If a team submission grouping id is provided all good as all returned groups
2440 // are the submission teams, but if no team submission grouping was specified
2441 // $groups will contain all participants groups.
2442 if ($this->get_instance()->teamsubmissiongroupingid) {
2444 // We restrict the users to the selected group ones.
2445 $groups = groups_get_all_groups($this->get_course()->id,
2446 array_keys($participants),
2447 $this->get_instance()->teamsubmissiongroupingid,
2448 'DISTINCT g.id, g.name');
2450 $count = count($groups);
2452 // When a specific group is selected we don't count the default group users.
2453 if ($activitygroup == 0) {
2454 if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2455 // See if there are any users in the default group.
2456 $defaultusers = $this->get_submission_group_members(0, true);
2457 if (count($defaultusers) > 0) {
2458 $count += 1;
2461 } else if ($activitygroup != 0 && empty($groups)) {
2462 // Set count to 1 if $groups returns empty.
2463 // It means the group is not part of $this->get_instance()->teamsubmissiongroupingid.
2464 $count = 1;
2466 } else {
2467 // It is faster to loop around participants if no grouping was specified.
2468 $groups = array();
2469 foreach ($participants as $participant) {
2470 if ($group = $this->get_submission_group($participant->id)) {
2471 $groups[$group->id] = true;
2472 } else if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2473 $groups[0] = true;
2477 $count = count($groups);
2480 return $count;
2484 * Load a count of active users enrolled in the current course with the specified permission and group.
2485 * 0 for no group.
2487 * @param int $currentgroup
2488 * @return int number of matching users
2490 public function count_participants($currentgroup) {
2491 return count($this->list_participants($currentgroup, true));
2495 * Load a count of active users submissions in the current module that require grading
2496 * This means the submission modification time is more recent than the
2497 * grading modification time and the status is SUBMITTED.
2499 * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
2500 * @return int number of matching submissions
2502 public function count_submissions_need_grading($currentgroup = null) {
2503 global $DB;
2505 if ($this->get_instance()->teamsubmission) {
2506 // This does not make sense for group assignment because the submission is shared.
2507 return 0;
2510 if ($currentgroup === null) {
2511 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2513 list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true);
2515 $params['assignid'] = $this->get_instance()->id;
2516 $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2517 $sqlscalegrade = $this->get_instance()->grade < 0 ? ' OR g.grade = -1' : '';
2519 $sql = 'SELECT COUNT(s.userid)
2520 FROM {assign_submission} s
2521 LEFT JOIN {assign_grades} g ON
2522 s.assignment = g.assignment AND
2523 s.userid = g.userid AND
2524 g.attemptnumber = s.attemptnumber
2525 JOIN(' . $esql . ') e ON e.id = s.userid
2526 WHERE
2527 s.latest = 1 AND
2528 s.assignment = :assignid AND
2529 s.timemodified IS NOT NULL AND
2530 s.status = :submitted AND
2531 (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL '
2532 . $sqlscalegrade . ')';
2534 return $DB->count_records_sql($sql, $params);
2538 * Load a count of grades.
2540 * @return int number of grades
2542 public function count_grades() {
2543 global $DB;
2545 if (!$this->has_instance()) {
2546 return 0;
2549 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2550 list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2552 $params['assignid'] = $this->get_instance()->id;
2554 $sql = 'SELECT COUNT(g.userid)
2555 FROM {assign_grades} g
2556 JOIN(' . $esql . ') e ON e.id = g.userid
2557 WHERE g.assignment = :assignid';
2559 return $DB->count_records_sql($sql, $params);
2563 * Load a count of submissions.
2565 * @param bool $includenew When true, also counts the submissions with status 'new'.
2566 * @return int number of submissions
2568 public function count_submissions($includenew = false) {
2569 global $DB;
2571 if (!$this->has_instance()) {
2572 return 0;
2575 $params = array();
2576 $sqlnew = '';
2578 if (!$includenew) {
2579 $sqlnew = ' AND s.status <> :status ';
2580 $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW;
2583 if ($this->get_instance()->teamsubmission) {
2584 // We cannot join on the enrolment tables for group submissions (no userid).
2585 $sql = 'SELECT COUNT(DISTINCT s.groupid)
2586 FROM {assign_submission} s
2587 WHERE
2588 s.assignment = :assignid AND
2589 s.timemodified IS NOT NULL AND
2590 s.userid = :groupuserid' .
2591 $sqlnew;
2593 $params['assignid'] = $this->get_instance()->id;
2594 $params['groupuserid'] = 0;
2595 } else {
2596 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2597 list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2599 $params = array_merge($params, $enrolparams);
2600 $params['assignid'] = $this->get_instance()->id;
2602 $sql = 'SELECT COUNT(DISTINCT s.userid)
2603 FROM {assign_submission} s
2604 JOIN(' . $esql . ') e ON e.id = s.userid
2605 WHERE
2606 s.assignment = :assignid AND
2607 s.timemodified IS NOT NULL ' .
2608 $sqlnew;
2612 return $DB->count_records_sql($sql, $params);
2616 * Load a count of submissions with a specified status.
2618 * @param string $status The submission status - should match one of the constants
2619 * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
2620 * @return int number of matching submissions
2622 public function count_submissions_with_status($status, $currentgroup = null) {
2623 global $DB;
2625 if ($currentgroup === null) {
2626 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2628 list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true);
2630 $params['assignid'] = $this->get_instance()->id;
2631 $params['assignid2'] = $this->get_instance()->id;
2632 $params['submissionstatus'] = $status;
2634 if ($this->get_instance()->teamsubmission) {
2636 $groupsstr = '';
2637 if ($currentgroup != 0) {
2638 // If there is an active group we should only display the current group users groups.
2639 $participants = $this->list_participants($currentgroup, true);
2640 $groups = groups_get_all_groups($this->get_course()->id,
2641 array_keys($participants),
2642 $this->get_instance()->teamsubmissiongroupingid,
2643 'DISTINCT g.id, g.name');
2644 if (empty($groups)) {
2645 // If $groups is empty it means it is not part of $this->get_instance()->teamsubmissiongroupingid.
2646 // All submissions from students that do not belong to any of teamsubmissiongroupingid groups
2647 // count towards groupid = 0. Setting to true as only '0' key matters.
2648 $groups = [true];
2650 list($groupssql, $groupsparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED);
2651 $groupsstr = 's.groupid ' . $groupssql . ' AND';
2652 $params = $params + $groupsparams;
2654 $sql = 'SELECT COUNT(s.groupid)
2655 FROM {assign_submission} s
2656 WHERE
2657 s.latest = 1 AND
2658 s.assignment = :assignid AND
2659 s.timemodified IS NOT NULL AND
2660 s.userid = :groupuserid AND '
2661 . $groupsstr . '
2662 s.status = :submissionstatus';
2663 $params['groupuserid'] = 0;
2664 } else {
2665 $sql = 'SELECT COUNT(s.userid)
2666 FROM {assign_submission} s
2667 JOIN(' . $esql . ') e ON e.id = s.userid
2668 WHERE
2669 s.latest = 1 AND
2670 s.assignment = :assignid AND
2671 s.timemodified IS NOT NULL AND
2672 s.status = :submissionstatus';
2676 return $DB->count_records_sql($sql, $params);
2680 * Utility function to get the userid for every row in the grading table
2681 * so the order can be frozen while we iterate it.
2683 * @param boolean $cached If true, the cached list from the session could be returned.
2684 * @param string $useridlistid String value used for caching the participant list.
2685 * @return array An array of userids
2687 protected function get_grading_userid_list($cached = false, $useridlistid = '') {
2688 global $SESSION;
2690 if ($cached) {
2691 if (empty($useridlistid)) {
2692 $useridlistid = $this->get_useridlist_key_id();
2694 $useridlistkey = $this->get_useridlist_key($useridlistid);
2695 if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) {
2696 $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list(false, '');
2698 return $SESSION->mod_assign_useridlist[$useridlistkey];
2700 $filter = get_user_preferences('assign_filter', '');
2701 $table = new assign_grading_table($this, 0, $filter, 0, false);
2703 $useridlist = $table->get_column_data('userid');
2705 return $useridlist;
2709 * Is user id filtered by user filters and table preferences.
2711 * @param int $userid User id that needs to be checked.
2712 * @return bool
2714 public function is_userid_filtered($userid) {
2715 $users = $this->get_grading_userid_list();
2716 return in_array($userid, $users);
2720 * Finds all assignment notifications that have yet to be mailed out, and mails them.
2722 * Cron function to be run periodically according to the moodle cron.
2724 * @return bool
2726 public static function cron() {
2727 global $DB;
2729 // Only ever send a max of one days worth of updates.
2730 $yesterday = time() - (24 * 3600);
2731 $timenow = time();
2732 $task = \core\task\manager::get_scheduled_task(mod_assign\task\cron_task::class);
2733 $lastruntime = $task->get_last_run_time();
2735 // Collect all submissions that require mailing.
2736 // Submissions are included if all are true:
2737 // - The assignment is visible in the gradebook.
2738 // - No previous notification has been sent.
2739 // - The grader was a real user, not an automated process.
2740 // - The grade was updated in the past 24 hours.
2741 // - If marking workflow is enabled, the workflow state is at 'released'.
2742 $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities, a.hidegrader,
2743 g.*, g.timemodified as lastmodified, cm.id as cmid, um.id as recordid
2744 FROM {assign} a
2745 JOIN {assign_grades} g ON g.assignment = a.id
2746 LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid
2747 JOIN {course_modules} cm ON cm.course = a.course AND cm.instance = a.id
2748 JOIN {modules} md ON md.id = cm.module AND md.name = 'assign'
2749 JOIN {grade_items} gri ON gri.iteminstance = a.id AND gri.courseid = a.course AND gri.itemmodule = md.name
2750 LEFT JOIN {assign_user_mapping} um ON g.id = um.userid AND um.assignment = a.id
2751 WHERE (a.markingworkflow = 0 OR (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND
2752 g.grader > 0 AND uf.mailed = 0 AND gri.hidden = 0 AND
2753 g.timemodified >= :yesterday AND g.timemodified <= :today
2754 ORDER BY a.course, cm.id";
2756 $params = array(
2757 'yesterday' => $yesterday,
2758 'today' => $timenow,
2759 'wfreleased' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
2761 $submissions = $DB->get_records_sql($sql, $params);
2763 if (!empty($submissions)) {
2765 mtrace('Processing ' . count($submissions) . ' assignment submissions ...');
2767 // Preload courses we are going to need those.
2768 $courseids = array();
2769 foreach ($submissions as $submission) {
2770 $courseids[] = $submission->course;
2773 // Filter out duplicates.
2774 $courseids = array_unique($courseids);
2775 $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
2776 list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
2777 $sql = 'SELECT c.*, ' . $ctxselect .
2778 ' FROM {course} c
2779 LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
2780 WHERE c.id ' . $courseidsql;
2782 $params['contextlevel'] = CONTEXT_COURSE;
2783 $courses = $DB->get_records_sql($sql, $params);
2785 // Clean up... this could go on for a while.
2786 unset($courseids);
2787 unset($ctxselect);
2788 unset($courseidsql);
2789 unset($params);
2791 // Message students about new feedback.
2792 foreach ($submissions as $submission) {
2794 mtrace("Processing assignment submission $submission->id ...");
2796 // Do not cache user lookups - could be too many.
2797 if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) {
2798 mtrace('Could not find user ' . $submission->userid);
2799 continue;
2802 // Use a cache to prevent the same DB queries happening over and over.
2803 if (!array_key_exists($submission->course, $courses)) {
2804 mtrace('Could not find course ' . $submission->course);
2805 continue;
2807 $course = $courses[$submission->course];
2808 if (isset($course->ctxid)) {
2809 // Context has not yet been preloaded. Do so now.
2810 context_helper::preload_from_record($course);
2813 // Override the language and timezone of the "current" user, so that
2814 // mail is customised for the receiver.
2815 \core\cron::setup_user($user, $course);
2817 // Context lookups are already cached.
2818 $coursecontext = context_course::instance($course->id);
2819 if (!is_enrolled($coursecontext, $user->id)) {
2820 $courseshortname = format_string($course->shortname,
2821 true,
2822 array('context' => $coursecontext));
2823 mtrace(fullname($user) . ' not an active participant in ' . $courseshortname);
2824 continue;
2827 if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) {
2828 mtrace('Could not find grader ' . $submission->grader);
2829 continue;
2832 $modinfo = get_fast_modinfo($course, $user->id);
2833 $cm = $modinfo->get_cm($submission->cmid);
2834 // Context lookups are already cached.
2835 $contextmodule = context_module::instance($cm->id);
2837 if (!$cm->uservisible) {
2838 // Hold mail notification for assignments the user cannot access until later.
2839 continue;
2842 // Notify the student. Default to the non-anon version.
2843 $messagetype = 'feedbackavailable';
2844 // Message type needs 'anon' if "hidden grading" is enabled and the student
2845 // doesn't have permission to see the grader.
2846 if ($submission->hidegrader && !has_capability('mod/assign:showhiddengrader', $contextmodule, $user)) {
2847 $messagetype = 'feedbackavailableanon';
2848 // There's no point in having an "anonymous grader" if the notification email
2849 // comes from them. Send the email from the noreply user instead.
2850 $grader = core_user::get_noreply_user();
2853 $eventtype = 'assign_notification';
2854 $updatetime = $submission->lastmodified;
2855 $modulename = get_string('modulename', 'assign');
2857 $uniqueid = 0;
2858 if ($submission->blindmarking && !$submission->revealidentities) {
2859 if (empty($submission->recordid)) {
2860 $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $grader->id);
2861 } else {
2862 $uniqueid = $submission->recordid;
2865 $showusers = $submission->blindmarking && !$submission->revealidentities;
2866 self::send_assignment_notification($grader,
2867 $user,
2868 $messagetype,
2869 $eventtype,
2870 $updatetime,
2871 $cm,
2872 $contextmodule,
2873 $course,
2874 $modulename,
2875 $submission->name,
2876 $showusers,
2877 $uniqueid);
2879 $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
2880 if ($flags) {
2881 $flags->mailed = 1;
2882 $DB->update_record('assign_user_flags', $flags);
2883 } else {
2884 $flags = new stdClass();
2885 $flags->userid = $user->id;
2886 $flags->assignment = $submission->assignment;
2887 $flags->mailed = 1;
2888 $DB->insert_record('assign_user_flags', $flags);
2891 mtrace('Done');
2893 mtrace('Done processing ' . count($submissions) . ' assignment submissions');
2895 \core\cron::setup_user();
2897 // Free up memory just to be sure.
2898 unset($courses);
2901 // Update calendar events to provide a description.
2902 $sql = 'SELECT id
2903 FROM {assign}
2904 WHERE
2905 allowsubmissionsfromdate >= :lastruntime AND
2906 allowsubmissionsfromdate <= :timenow AND
2907 alwaysshowdescription = 0';
2908 $params = array('lastruntime' => $lastruntime, 'timenow' => $timenow);
2909 $newlyavailable = $DB->get_records_sql($sql, $params);
2910 foreach ($newlyavailable as $record) {
2911 $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST);
2912 $context = context_module::instance($cm->id);
2914 $assignment = new assign($context, null, null);
2915 $assignment->update_calendar($cm->id);
2918 return true;
2922 * Mark in the database that this grade record should have an update notification sent by cron.
2924 * @param stdClass $grade a grade record keyed on id
2925 * @param bool $mailedoverride when true, flag notification to be sent again.
2926 * @return bool true for success
2928 public function notify_grade_modified($grade, $mailedoverride = false) {
2929 global $DB;
2931 $flags = $this->get_user_flags($grade->userid, true);
2932 if ($flags->mailed != 1 || $mailedoverride) {
2933 $flags->mailed = 0;
2936 return $this->update_user_flags($flags);
2940 * Update user flags for this user in this assignment.
2942 * @param stdClass $flags a flags record keyed on id
2943 * @return bool true for success
2945 public function update_user_flags($flags) {
2946 global $DB;
2947 if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) {
2948 return false;
2951 $result = $DB->update_record('assign_user_flags', $flags);
2952 return $result;
2956 * Update a grade in the grade table for the assignment and in the gradebook.
2958 * @param stdClass $grade a grade record keyed on id
2959 * @param bool $reopenattempt If the attempt reopen method is manual, allow another attempt at this assignment.
2960 * @return bool true for success
2962 public function update_grade($grade, $reopenattempt = false) {
2963 global $DB;
2965 $grade->timemodified = time();
2967 if (!empty($grade->workflowstate)) {
2968 $validstates = $this->get_marking_workflow_states_for_current_user();
2969 if (!array_key_exists($grade->workflowstate, $validstates)) {
2970 return false;
2974 if ($grade->grade && $grade->grade != -1) {
2975 if ($this->get_instance()->grade > 0) {
2976 if (!is_numeric($grade->grade)) {
2977 return false;
2978 } else if ($grade->grade > $this->get_instance()->grade) {
2979 return false;
2980 } else if ($grade->grade < 0) {
2981 return false;
2983 } else {
2984 // This is a scale.
2985 if ($scale = $DB->get_record('scale', array('id' => -($this->get_instance()->grade)))) {
2986 $scaleoptions = make_menu_from_list($scale->scale);
2987 if (!array_key_exists((int) $grade->grade, $scaleoptions)) {
2988 return false;
2994 if (empty($grade->attemptnumber)) {
2995 // Set it to the default.
2996 $grade->attemptnumber = 0;
2998 $DB->update_record('assign_grades', $grade);
3000 $submission = null;
3001 if ($this->get_instance()->teamsubmission) {
3002 if (isset($this->mostrecentteamsubmission)) {
3003 $submission = $this->mostrecentteamsubmission;
3004 } else {
3005 $submission = $this->get_group_submission($grade->userid, 0, false);
3007 } else {
3008 $submission = $this->get_user_submission($grade->userid, false);
3011 // Only push to gradebook if the update is for the most recent attempt.
3012 if ($submission && $submission->attemptnumber != $grade->attemptnumber) {
3013 return true;
3016 if ($this->gradebook_item_update(null, $grade)) {
3017 \mod_assign\event\submission_graded::create_from_grade($this, $grade)->trigger();
3020 // If the conditions are met, allow another attempt.
3021 if ($submission) {
3022 $isreopened = $this->reopen_submission_if_required($grade->userid,
3023 $submission,
3024 $reopenattempt);
3025 if ($isreopened) {
3026 $completion = new completion_info($this->get_course());
3027 if ($completion->is_enabled($this->get_course_module()) &&
3028 $this->get_instance()->completionsubmit) {
3029 $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $grade->userid);
3034 return true;
3038 * View the grant extension date page.
3040 * Uses url parameters 'userid'
3041 * or from parameter 'selectedusers'
3043 * @param moodleform $mform - Used for validation of the submitted data
3044 * @return string
3046 protected function view_grant_extension($mform) {
3047 global $CFG;
3048 require_once($CFG->dirroot . '/mod/assign/extensionform.php');
3050 $o = '';
3052 $data = new stdClass();
3053 $data->id = $this->get_course_module()->id;
3055 $formparams = array(
3056 'instance' => $this->get_instance(),
3057 'assign' => $this
3060 $users = optional_param('userid', 0, PARAM_INT);
3061 if (!$users) {
3062 $users = required_param('selectedusers', PARAM_SEQUENCE);
3064 $userlist = explode(',', $users);
3066 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
3067 $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
3068 foreach ($userlist as $userid) {
3069 // To validate extension date with users overrides.
3070 $override = $this->override_exists($userid);
3071 foreach ($keys as $key) {
3072 if ($override->{$key}) {
3073 if ($maxoverride[$key] < $override->{$key}) {
3074 $maxoverride[$key] = $override->{$key};
3076 } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
3077 $maxoverride[$key] = $this->get_instance()->{$key};
3081 foreach ($keys as $key) {
3082 if ($maxoverride[$key]) {
3083 $this->get_instance()->{$key} = $maxoverride[$key];
3087 $formparams['userlist'] = $userlist;
3089 $data->selectedusers = $users;
3090 $data->userid = 0;
3092 if (empty($mform)) {
3093 $mform = new mod_assign_extension_form(null, $formparams);
3095 $mform->set_data($data);
3096 $header = new assign_header($this->get_instance(),
3097 $this->get_context(),
3098 $this->show_intro(),
3099 $this->get_course_module()->id,
3100 get_string('grantextension', 'assign'));
3101 $o .= $this->get_renderer()->render($header);
3102 $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform));
3103 $o .= $this->view_footer();
3104 return $o;
3108 * Get a list of the users in the same group as this user.
3110 * @param int $groupid The id of the group whose members we want or 0 for the default group
3111 * @param bool $onlyids Whether to retrieve only the user id's
3112 * @param bool $excludesuspended Whether to exclude suspended users
3113 * @return array The users (possibly id's only)
3115 public function get_submission_group_members($groupid, $onlyids, $excludesuspended = false) {
3116 $members = array();
3117 if ($groupid != 0) {
3118 $allusers = $this->list_participants($groupid, $onlyids);
3119 foreach ($allusers as $user) {
3120 if ($this->get_submission_group($user->id)) {
3121 $members[] = $user;
3124 } else {
3125 $allusers = $this->list_participants(null, $onlyids);
3126 foreach ($allusers as $user) {
3127 if ($this->get_submission_group($user->id) == null) {
3128 $members[] = $user;
3132 // Exclude suspended users, if user can't see them.
3133 if ($excludesuspended || !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
3134 foreach ($members as $key => $member) {
3135 if (!$this->is_active_user($member->id)) {
3136 unset($members[$key]);
3141 return $members;
3145 * Get a list of the users in the same group as this user that have not submitted the assignment.
3147 * @param int $groupid The id of the group whose members we want or 0 for the default group
3148 * @param bool $onlyids Whether to retrieve only the user id's
3149 * @return array The users (possibly id's only)
3151 public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) {
3152 $instance = $this->get_instance();
3153 if (!$instance->teamsubmission || !$instance->requireallteammemberssubmit) {
3154 return array();
3156 $members = $this->get_submission_group_members($groupid, $onlyids);
3158 foreach ($members as $id => $member) {
3159 $submission = $this->get_user_submission($member->id, false);
3160 if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
3161 unset($members[$id]);
3162 } else {
3163 if ($this->is_blind_marking()) {
3164 $members[$id]->alias = get_string('hiddenuser', 'assign') .
3165 $this->get_uniqueid_for_user($id);
3169 return $members;
3173 * Load the group submission object for a particular user, optionally creating it if required.
3175 * @param int $userid The id of the user whose submission we want
3176 * @param int $groupid The id of the group for this user - may be 0 in which
3177 * case it is determined from the userid.
3178 * @param bool $create If set to true a new submission object will be created in the database
3179 * with the status set to "new".
3180 * @param int $attemptnumber - -1 means the latest attempt
3181 * @return stdClass|false The submission
3183 public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) {
3184 global $DB;
3186 if ($groupid == 0) {
3187 $group = $this->get_submission_group($userid);
3188 if ($group) {
3189 $groupid = $group->id;
3193 // Now get the group submission.
3194 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
3195 if ($attemptnumber >= 0) {
3196 $params['attemptnumber'] = $attemptnumber;
3199 // Only return the row with the highest attemptnumber.
3200 $submission = null;
3201 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
3202 if ($submissions) {
3203 $submission = reset($submissions);
3206 if ($submission) {
3207 if ($create) {
3208 $action = optional_param('action', '', PARAM_TEXT);
3209 if ($action == 'editsubmission') {
3210 if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
3211 $submission->timestarted = time();
3212 $DB->update_record('assign_submission', $submission);
3216 return $submission;
3218 if ($create) {
3219 $submission = new stdClass();
3220 $submission->assignment = $this->get_instance()->id;
3221 $submission->userid = 0;
3222 $submission->groupid = $groupid;
3223 $submission->timecreated = time();
3224 $submission->timemodified = $submission->timecreated;
3225 if ($attemptnumber >= 0) {
3226 $submission->attemptnumber = $attemptnumber;
3227 } else {
3228 $submission->attemptnumber = 0;
3230 // Work out if this is the latest submission.
3231 $submission->latest = 0;
3232 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
3233 if ($attemptnumber == -1) {
3234 // This is a new submission so it must be the latest.
3235 $submission->latest = 1;
3236 } else {
3237 // We need to work this out.
3238 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
3239 if ($result) {
3240 $latestsubmission = reset($result);
3242 if (!$latestsubmission || ($attemptnumber == $latestsubmission->attemptnumber)) {
3243 $submission->latest = 1;
3246 $transaction = $DB->start_delegated_transaction();
3247 if ($submission->latest) {
3248 // This is the case when we need to set latest to 0 for all the other attempts.
3249 $DB->set_field('assign_submission', 'latest', 0, $params);
3251 $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
3252 $sid = $DB->insert_record('assign_submission', $submission);
3253 $transaction->allow_commit();
3254 return $DB->get_record('assign_submission', array('id' => $sid));
3256 return false;
3260 * View a summary listing of all assignments in the current course.
3262 * @return string
3264 private function view_course_index() {
3265 global $USER;
3267 $o = '';
3269 $course = $this->get_course();
3270 $strplural = get_string('modulenameplural', 'assign');
3272 if (!$cms = get_coursemodules_in_course('assign', $course->id, 'm.duedate')) {
3273 $o .= $this->get_renderer()->notification(get_string('thereareno', 'moodle', $strplural));
3274 $o .= $this->get_renderer()->continue_button(new moodle_url('/course/view.php', array('id' => $course->id)));
3275 return $o;
3278 $strsectionname = '';
3279 $usesections = course_format_uses_sections($course->format);
3280 $modinfo = get_fast_modinfo($course);
3282 if ($usesections) {
3283 $strsectionname = get_string('sectionname', 'format_'.$course->format);
3284 $sections = $modinfo->get_section_info_all();
3286 $courseindexsummary = new assign_course_index_summary($usesections, $strsectionname);
3288 $timenow = time();
3290 $currentsection = '';
3291 foreach ($modinfo->instances['assign'] as $cm) {
3292 if (!$cm->uservisible) {
3293 continue;
3296 $timedue = $cms[$cm->id]->duedate;
3298 $sectionname = '';
3299 if ($usesections && $cm->sectionnum) {
3300 $sectionname = get_section_name($course, $sections[$cm->sectionnum]);
3303 $submitted = '';
3304 $context = context_module::instance($cm->id);
3306 $assignment = new assign($context, $cm, $course);
3308 // Apply overrides.
3309 $assignment->update_effective_access($USER->id);
3310 $timedue = $assignment->get_instance()->duedate;
3312 if (has_capability('mod/assign:submit', $context) &&
3313 !has_capability('moodle/site:config', $context)) {
3314 $cangrade = false;
3315 if ($assignment->get_instance()->teamsubmission) {
3316 $usersubmission = $assignment->get_group_submission($USER->id, 0, false);
3317 } else {
3318 $usersubmission = $assignment->get_user_submission($USER->id, false);
3321 if (!empty($usersubmission->status)) {
3322 $submitted = get_string('submissionstatus_' . $usersubmission->status, 'assign');
3323 } else {
3324 $submitted = get_string('submissionstatus_', 'assign');
3327 $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $cm->instance, $USER->id);
3328 if (isset($gradinginfo->items[0]->grades[$USER->id]) &&
3329 !$gradinginfo->items[0]->grades[$USER->id]->hidden ) {
3330 $grade = $gradinginfo->items[0]->grades[$USER->id]->str_grade;
3331 } else {
3332 $grade = '-';
3334 } else if (has_capability('mod/assign:grade', $context)) {
3335 $submitted = $assignment->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED);
3336 $grade = $assignment->count_submissions_need_grading();
3337 $cangrade = true;
3340 $courseindexsummary->add_assign_info($cm->id, $cm->get_formatted_name(),
3341 $sectionname, $timedue, $submitted, $grade, $cangrade);
3344 $o .= $this->get_renderer()->render($courseindexsummary);
3345 $o .= $this->view_footer();
3347 return $o;
3351 * View a page rendered by a plugin.
3353 * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'.
3355 * @return string
3357 protected function view_plugin_page() {
3358 global $USER;
3360 $o = '';
3362 $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
3363 $plugintype = required_param('plugin', PARAM_PLUGIN);
3364 $pluginaction = required_param('pluginaction', PARAM_ALPHA);
3366 $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
3367 if (!$plugin) {
3368 throw new \moodle_exception('invalidformdata', '');
3369 return;
3372 $o .= $plugin->view_page($pluginaction);
3374 return $o;
3379 * This is used for team assignments to get the group for the specified user.
3380 * If the user is a member of multiple or no groups this will return false
3382 * @param int $userid The id of the user whose submission we want
3383 * @return mixed The group or false
3385 public function get_submission_group($userid) {
3387 if (isset($this->usersubmissiongroups[$userid])) {
3388 return $this->usersubmissiongroups[$userid];
3391 $groups = $this->get_all_groups($userid);
3392 if (count($groups) != 1) {
3393 $return = false;
3394 } else {
3395 $return = array_pop($groups);
3398 // Cache the user submission group.
3399 $this->usersubmissiongroups[$userid] = $return;
3401 return $return;
3405 * Gets all groups the user is a member of.
3407 * @param int $userid Teh id of the user who's groups we are checking
3408 * @return array The group objects
3410 public function get_all_groups($userid) {
3411 if (isset($this->usergroups[$userid])) {
3412 return $this->usergroups[$userid];
3415 $grouping = $this->get_instance()->teamsubmissiongroupingid;
3416 $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping, 'g.*', false, true);
3418 $this->usergroups[$userid] = $return;
3420 return $return;
3425 * Display the submission that is used by a plugin.
3427 * Uses url parameters 'sid', 'gid' and 'plugin'.
3429 * @param string $pluginsubtype
3430 * @return string
3432 protected function view_plugin_content($pluginsubtype) {
3433 $o = '';
3435 $submissionid = optional_param('sid', 0, PARAM_INT);
3436 $gradeid = optional_param('gid', 0, PARAM_INT);
3437 $plugintype = required_param('plugin', PARAM_PLUGIN);
3438 $item = null;
3439 if ($pluginsubtype == 'assignsubmission') {
3440 $plugin = $this->get_submission_plugin_by_type($plugintype);
3441 if ($submissionid <= 0) {
3442 throw new coding_exception('Submission id should not be 0');
3444 $item = $this->get_submission($submissionid);
3446 // Check permissions.
3447 if (empty($item->userid)) {
3448 // Group submission.
3449 $this->require_view_group_submission($item->groupid);
3450 } else {
3451 $this->require_view_submission($item->userid);
3453 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3454 $this->get_context(),
3455 $this->show_intro(),
3456 $this->get_course_module()->id,
3457 $plugin->get_name()));
3458 $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin,
3459 $item,
3460 assign_submission_plugin_submission::FULL,
3461 $this->get_course_module()->id,
3462 $this->get_return_action(),
3463 $this->get_return_params()));
3465 // Trigger event for viewing a submission.
3466 \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger();
3468 } else {
3469 $plugin = $this->get_feedback_plugin_by_type($plugintype);
3470 if ($gradeid <= 0) {
3471 throw new coding_exception('Grade id should not be 0');
3473 $item = $this->get_grade($gradeid);
3474 // Check permissions.
3475 $this->require_view_submission($item->userid);
3476 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3477 $this->get_context(),
3478 $this->show_intro(),
3479 $this->get_course_module()->id,
3480 $plugin->get_name()));
3481 $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin,
3482 $item,
3483 assign_feedback_plugin_feedback::FULL,
3484 $this->get_course_module()->id,
3485 $this->get_return_action(),
3486 $this->get_return_params()));
3488 // Trigger event for viewing feedback.
3489 \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger();
3492 $o .= $this->view_return_links();
3494 $o .= $this->view_footer();
3496 return $o;
3500 * Rewrite plugin file urls so they resolve correctly in an exported zip.
3502 * @param string $text - The replacement text
3503 * @param stdClass $user - The user record
3504 * @param assign_plugin $plugin - The assignment plugin
3506 public function download_rewrite_pluginfile_urls($text, $user, $plugin) {
3507 // The groupname prefix for the urls doesn't depend on the group mode of the assignment instance.
3508 // Rather, it should be determined by checking the group submission settings of the instance,
3509 // which is what download_submission() does when generating the file name prefixes.
3510 $groupname = '';
3511 if ($this->get_instance()->teamsubmission) {
3512 $submissiongroup = $this->get_submission_group($user->id);
3513 if ($submissiongroup) {
3514 $groupname = $submissiongroup->name . '-';
3515 } else {
3516 $groupname = get_string('defaultteam', 'assign') . '-';
3520 if ($this->is_blind_marking()) {
3521 $prefix = $groupname . get_string('participant', 'assign');
3522 $prefix = str_replace('_', ' ', $prefix);
3523 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3524 } else {
3525 $prefix = $groupname . fullname($user);
3526 $prefix = str_replace('_', ' ', $prefix);
3527 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3530 // Only prefix files if downloadasfolders user preference is NOT set.
3531 if (!get_user_preferences('assign_downloadasfolders', 1)) {
3532 $subtype = $plugin->get_subtype();
3533 $type = $plugin->get_type();
3534 $prefix = $prefix . $subtype . '_' . $type . '_';
3535 } else {
3536 $prefix = "";
3538 $result = str_replace('@@PLUGINFILE@@/', $prefix, $text);
3540 return $result;
3544 * Render the content in editor that is often used by plugin.
3546 * @param string $filearea
3547 * @param int $submissionid
3548 * @param string $plugintype
3549 * @param string $editor
3550 * @param string $component
3551 * @param bool $shortentext Whether to shorten the text content.
3552 * @return string
3554 public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component, $shortentext = false) {
3555 global $CFG;
3557 $result = '';
3559 $plugin = $this->get_submission_plugin_by_type($plugintype);
3561 $text = $plugin->get_editor_text($editor, $submissionid);
3562 if ($shortentext) {
3563 $text = shorten_text($text, 140);
3565 $format = $plugin->get_editor_format($editor, $submissionid);
3567 $finaltext = file_rewrite_pluginfile_urls($text,
3568 'pluginfile.php',
3569 $this->get_context()->id,
3570 $component,
3571 $filearea,
3572 $submissionid);
3573 $params = array('overflowdiv' => true, 'context' => $this->get_context());
3574 $result .= format_text($finaltext, $format, $params);
3576 if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) {
3577 require_once($CFG->libdir . '/portfoliolib.php');
3579 $button = new portfolio_add_button();
3580 $portfolioparams = array('cmid' => $this->get_course_module()->id,
3581 'sid' => $submissionid,
3582 'plugin' => $plugintype,
3583 'editor' => $editor,
3584 'area'=>$filearea);
3585 $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign');
3586 $fs = get_file_storage();
3588 if ($files = $fs->get_area_files($this->context->id,
3589 $component,
3590 $filearea,
3591 $submissionid,
3592 'timemodified',
3593 false)) {
3594 $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3595 } else {
3596 $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
3598 $result .= $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
3600 return $result;
3604 * Display a continue page after grading.
3606 * @param string $message - The message to display.
3607 * @return string
3609 protected function view_savegrading_result($message) {
3610 $o = '';
3611 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3612 $this->get_context(),
3613 $this->show_intro(),
3614 $this->get_course_module()->id,
3615 get_string('savegradingresult', 'assign')));
3616 $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'),
3617 $message,
3618 $this->get_course_module()->id);
3619 $o .= $this->get_renderer()->render($gradingresult);
3620 $o .= $this->view_footer();
3621 return $o;
3624 * Display a continue page after quickgrading.
3626 * @param string $message - The message to display.
3627 * @return string
3629 protected function view_quickgrading_result($message) {
3630 $o = '';
3631 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3632 $this->get_context(),
3633 $this->show_intro(),
3634 $this->get_course_module()->id,
3635 get_string('quickgradingresult', 'assign')));
3636 $gradingerror = in_array($message, $this->get_error_messages());
3637 $lastpage = optional_param('lastpage', null, PARAM_INT);
3638 $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'),
3639 $message,
3640 $this->get_course_module()->id,
3641 $gradingerror,
3642 $lastpage);
3643 $o .= $this->get_renderer()->render($gradingresult);
3644 $o .= $this->view_footer();
3645 return $o;
3649 * Display the page footer.
3651 * @return string
3653 protected function view_footer() {
3654 // When viewing the footer during PHPUNIT tests a set_state error is thrown.
3655 if (!PHPUNIT_TEST) {
3656 return $this->get_renderer()->render_footer();
3659 return '';
3663 * Throw an error if the permissions to view this users' group submission are missing.
3665 * @param int $groupid Group id.
3666 * @throws required_capability_exception
3668 public function require_view_group_submission($groupid) {
3669 if (!$this->can_view_group_submission($groupid)) {
3670 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3675 * Throw an error if the permissions to view this users submission are missing.
3677 * @throws required_capability_exception
3678 * @return none
3680 public function require_view_submission($userid) {
3681 if (!$this->can_view_submission($userid)) {
3682 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3687 * Throw an error if the permissions to view grades in this assignment are missing.
3689 * @throws required_capability_exception
3690 * @return none
3692 public function require_view_grades() {
3693 if (!$this->can_view_grades()) {
3694 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3699 * Does this user have view grade or grade permission for this assignment?
3701 * @param mixed $groupid int|null when is set to a value, use this group instead calculating it
3702 * @return bool
3704 public function can_view_grades($groupid = null) {
3705 // Permissions check.
3706 if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
3707 return false;
3709 // Checks for the edge case when user belongs to no groups and groupmode is sep.
3710 if ($this->get_course_module()->effectivegroupmode == SEPARATEGROUPS) {
3711 if ($groupid === null) {
3712 $groupid = groups_get_activity_allowed_groups($this->get_course_module());
3714 $groupflag = has_capability('moodle/site:accessallgroups', $this->get_context());
3715 $groupflag = $groupflag || !empty($groupid);
3716 return (bool)$groupflag;
3718 return true;
3722 * Does this user have grade permission for this assignment?
3724 * @param int|stdClass $user The object or id of the user who will do the editing (default to current user).
3725 * @return bool
3727 public function can_grade($user = null) {
3728 // Permissions check.
3729 if (!has_capability('mod/assign:grade', $this->context, $user)) {
3730 return false;
3733 return true;
3737 * Download a zip file of all assignment submissions.
3739 * @param int[]|null $userids Array of user ids to download assignment submissions in a zip file
3740 * @return string - If an error occurs, this will contain the error page.
3742 protected function download_submissions($userids = null) {
3743 $downloader = new downloader($this, $userids ?: null);
3744 if ($downloader->load_filelist()) {
3745 $downloader->download_zip();
3747 // Show some notification if we have nothing to download.
3748 $cm = $this->get_course_module();
3749 $renderer = $this->get_renderer();
3750 $header = new assign_header(
3751 $this->get_instance(),
3752 $this->get_context(),
3754 $cm->id,
3755 get_string('downloadall', 'mod_assign')
3757 $result = $renderer->render($header);
3758 $result .= $renderer->notification(get_string('nosubmission', 'mod_assign'));
3759 $url = new moodle_url('/mod/assign/view.php', ['id' => $cm->id, 'action' => 'grading']);
3760 $result .= $renderer->continue_button($url);
3761 $result .= $this->view_footer();
3762 return $result;
3766 * @deprecated since 2.7 - Use new events system instead.
3768 public function add_to_log() {
3769 throw new coding_exception(__FUNCTION__ . ' has been deprecated, please do not use it any more');
3773 * Lazy load the page renderer and expose the renderer to plugins.
3775 * @return assign_renderer
3777 public function get_renderer() {
3778 global $PAGE;
3779 if ($this->output) {
3780 return $this->output;
3782 $this->output = $PAGE->get_renderer('mod_assign', null, RENDERER_TARGET_GENERAL);
3783 return $this->output;
3787 * Load the submission object for a particular user, optionally creating it if required.
3789 * For team assignments there are 2 submissions - the student submission and the team submission
3790 * All files are associated with the team submission but the status of the students contribution is
3791 * recorded separately.
3793 * @param int $userid The id of the user whose submission we want or 0 in which case USER->id is used
3794 * @param bool $create If set to true a new submission object will be created in the database with the status set to "new".
3795 * @param int $attemptnumber - -1 means the latest attempt
3796 * @return stdClass|false The submission
3798 public function get_user_submission($userid, $create, $attemptnumber=-1) {
3799 global $DB, $USER;
3801 if (!$userid) {
3802 $userid = $USER->id;
3804 // If the userid is not null then use userid.
3805 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
3806 if ($attemptnumber >= 0) {
3807 $params['attemptnumber'] = $attemptnumber;
3810 // Only return the row with the highest attemptnumber.
3811 $submission = null;
3812 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
3813 if ($submissions) {
3814 $submission = reset($submissions);
3817 if ($submission) {
3818 if ($create) {
3819 $action = optional_param('action', '', PARAM_TEXT);
3820 if ($action == 'editsubmission') {
3821 if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
3822 $submission->timestarted = time();
3823 $DB->update_record('assign_submission', $submission);
3827 return $submission;
3829 if ($create) {
3830 $submission = new stdClass();
3831 $submission->assignment = $this->get_instance()->id;
3832 $submission->userid = $userid;
3833 $submission->timecreated = time();
3834 $submission->timemodified = $submission->timecreated;
3835 $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
3836 if ($attemptnumber >= 0) {
3837 $submission->attemptnumber = $attemptnumber;
3838 } else {
3839 $submission->attemptnumber = 0;
3841 // Work out if this is the latest submission.
3842 $submission->latest = 0;
3843 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
3844 if ($attemptnumber == -1) {
3845 // This is a new submission so it must be the latest.
3846 $submission->latest = 1;
3847 } else {
3848 // We need to work this out.
3849 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
3850 $latestsubmission = null;
3851 if ($result) {
3852 $latestsubmission = reset($result);
3854 if (empty($latestsubmission) || ($attemptnumber > $latestsubmission->attemptnumber)) {
3855 $submission->latest = 1;
3858 $transaction = $DB->start_delegated_transaction();
3859 if ($submission->latest) {
3860 // This is the case when we need to set latest to 0 for all the other attempts.
3861 $DB->set_field('assign_submission', 'latest', 0, $params);
3863 $sid = $DB->insert_record('assign_submission', $submission);
3864 $transaction->allow_commit();
3865 return $DB->get_record('assign_submission', array('id' => $sid));
3867 return false;
3871 * Load the submission object from it's id.
3873 * @param int $submissionid The id of the submission we want
3874 * @return stdClass The submission
3876 protected function get_submission($submissionid) {
3877 global $DB;
3879 $params = array('assignment'=>$this->get_instance()->id, 'id'=>$submissionid);
3880 return $DB->get_record('assign_submission', $params, '*', MUST_EXIST);
3884 * This will retrieve a user flags object from the db optionally creating it if required.
3885 * The user flags was split from the user_grades table in 2.5.
3887 * @param int $userid The user we are getting the flags for.
3888 * @param bool $create If true the flags record will be created if it does not exist
3889 * @return stdClass The flags record
3891 public function get_user_flags($userid, $create) {
3892 global $DB, $USER;
3894 // If the userid is not null then use userid.
3895 if (!$userid) {
3896 $userid = $USER->id;
3899 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
3901 $flags = $DB->get_record('assign_user_flags', $params);
3903 if ($flags) {
3904 return $flags;
3906 if ($create) {
3907 $flags = new stdClass();
3908 $flags->assignment = $this->get_instance()->id;
3909 $flags->userid = $userid;
3910 $flags->locked = 0;
3911 $flags->extensionduedate = 0;
3912 $flags->workflowstate = '';
3913 $flags->allocatedmarker = 0;
3915 // The mailed flag can be one of 3 values: 0 is unsent, 1 is sent and 2 is do not send yet.
3916 // This is because students only want to be notified about certain types of update (grades and feedback).
3917 $flags->mailed = 2;
3919 $fid = $DB->insert_record('assign_user_flags', $flags);
3920 $flags->id = $fid;
3921 return $flags;
3923 return false;
3927 * This will retrieve a grade object from the db, optionally creating it if required.
3929 * @param int $userid The user we are grading
3930 * @param bool $create If true the grade will be created if it does not exist
3931 * @param int $attemptnumber The attempt number to retrieve the grade for. -1 means the latest submission.
3932 * @return stdClass The grade record
3934 public function get_user_grade($userid, $create, $attemptnumber=-1) {
3935 global $DB, $USER;
3937 // If the userid is not null then use userid.
3938 if (!$userid) {
3939 $userid = $USER->id;
3941 $submission = null;
3943 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
3944 if ($attemptnumber < 0 || $create) {
3945 // Make sure this grade matches the latest submission attempt.
3946 if ($this->get_instance()->teamsubmission) {
3947 $submission = $this->get_group_submission($userid, 0, true, $attemptnumber);
3948 } else {
3949 $submission = $this->get_user_submission($userid, true, $attemptnumber);
3951 if ($submission) {
3952 $attemptnumber = $submission->attemptnumber;
3956 if ($attemptnumber >= 0) {
3957 $params['attemptnumber'] = $attemptnumber;
3960 $grades = $DB->get_records('assign_grades', $params, 'attemptnumber DESC', '*', 0, 1);
3962 if ($grades) {
3963 return reset($grades);
3965 if ($create) {
3966 $grade = new stdClass();
3967 $grade->assignment = $this->get_instance()->id;
3968 $grade->userid = $userid;
3969 $grade->timecreated = time();
3970 // If we are "auto-creating" a grade - and there is a submission
3971 // the new grade should not have a more recent timemodified value
3972 // than the submission.
3973 if ($submission) {
3974 $grade->timemodified = $submission->timemodified;
3975 } else {
3976 $grade->timemodified = $grade->timecreated;
3978 $grade->grade = -1;
3979 // Do not set the grader id here as it would be the admin users which is incorrect.
3980 $grade->grader = -1;
3981 if ($attemptnumber >= 0) {
3982 $grade->attemptnumber = $attemptnumber;
3985 $gid = $DB->insert_record('assign_grades', $grade);
3986 $grade->id = $gid;
3987 return $grade;
3989 return false;
3993 * This will retrieve a grade object from the db.
3995 * @param int $gradeid The id of the grade
3996 * @return stdClass The grade record
3998 protected function get_grade($gradeid) {
3999 global $DB;
4001 $params = array('assignment'=>$this->get_instance()->id, 'id'=>$gradeid);
4002 return $DB->get_record('assign_grades', $params, '*', MUST_EXIST);
4006 * Print the grading page for a single user submission.
4008 * @param array $args Optional args array (better than pulling args from _GET and _POST)
4009 * @return string
4011 protected function view_single_grading_panel($args) {
4012 global $DB, $CFG;
4014 $o = '';
4016 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
4018 // Need submit permission to submit an assignment.
4019 require_capability('mod/assign:grade', $this->context);
4021 // If userid is passed - we are only grading a single student.
4022 $userid = $args['userid'];
4023 $attemptnumber = $args['attemptnumber'];
4024 $instance = $this->get_instance($userid);
4026 // Apply overrides.
4027 $this->update_effective_access($userid);
4029 $rownum = 0;
4030 $useridlist = array($userid);
4032 $last = true;
4033 // This variation on the url will link direct to this student, with no next/previous links.
4034 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4035 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4036 $this->register_return_link('grade', $returnparams);
4038 $user = $DB->get_record('user', array('id' => $userid));
4039 $submission = $this->get_user_submission($userid, false, $attemptnumber);
4040 $submissiongroup = null;
4041 $teamsubmission = null;
4042 $notsubmitted = array();
4043 if ($instance->teamsubmission) {
4044 $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
4045 $submissiongroup = $this->get_submission_group($userid);
4046 $groupid = 0;
4047 if ($submissiongroup) {
4048 $groupid = $submissiongroup->id;
4050 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
4054 // Get the requested grade.
4055 $grade = $this->get_user_grade($userid, false, $attemptnumber);
4056 $flags = $this->get_user_flags($userid, false);
4057 if ($this->can_view_submission($userid)) {
4058 $submissionlocked = ($flags && $flags->locked);
4059 $extensionduedate = null;
4060 if ($flags) {
4061 $extensionduedate = $flags->extensionduedate;
4063 $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
4064 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4065 $usergroups = $this->get_all_groups($user->id);
4067 $submissionstatus = new assign_submission_status_compact($instance->allowsubmissionsfromdate,
4068 $instance->alwaysshowdescription,
4069 $submission,
4070 $instance->teamsubmission,
4071 $teamsubmission,
4072 $submissiongroup,
4073 $notsubmitted,
4074 $this->is_any_submission_plugin_enabled(),
4075 $submissionlocked,
4076 $this->is_graded($userid),
4077 $instance->duedate,
4078 $instance->cutoffdate,
4079 $this->get_submission_plugins(),
4080 $this->get_return_action(),
4081 $this->get_return_params(),
4082 $this->get_course_module()->id,
4083 $this->get_course()->id,
4084 assign_submission_status::GRADER_VIEW,
4085 $showedit,
4086 false,
4087 $viewfullnames,
4088 $extensionduedate,
4089 $this->get_context(),
4090 $this->is_blind_marking(),
4092 $instance->attemptreopenmethod,
4093 $instance->maxattempts,
4094 $this->get_grading_status($userid),
4095 $instance->preventsubmissionnotingroup,
4096 $usergroups,
4097 $instance->timelimit);
4098 $o .= $this->get_renderer()->render($submissionstatus);
4101 if ($grade) {
4102 $data = new stdClass();
4103 if ($grade->grade !== null && $grade->grade >= 0) {
4104 $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
4106 } else {
4107 $data = new stdClass();
4108 $data->grade = '';
4111 if (!empty($flags->workflowstate)) {
4112 $data->workflowstate = $flags->workflowstate;
4114 if (!empty($flags->allocatedmarker)) {
4115 $data->allocatedmarker = $flags->allocatedmarker;
4118 // Warning if required.
4119 $allsubmissions = $this->get_all_submissions($userid);
4121 if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
4122 $params = array('attemptnumber' => $attemptnumber + 1,
4123 'totalattempts' => count($allsubmissions));
4124 $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
4125 $o .= $this->get_renderer()->notification($message);
4128 $pagination = array('rownum' => $rownum,
4129 'useridlistid' => 0,
4130 'last' => $last,
4131 'userid' => $userid,
4132 'attemptnumber' => $attemptnumber,
4133 'gradingpanel' => true);
4135 if (!empty($args['formdata'])) {
4136 $data = (array) $data;
4137 $data = (object) array_merge($data, $args['formdata']);
4139 $formparams = array($this, $data, $pagination);
4140 $mform = new mod_assign_grade_form(null,
4141 $formparams,
4142 'post',
4144 array('class' => 'gradeform'));
4146 if (!empty($args['formdata'])) {
4147 // If we were passed form data - we want the form to check the data
4148 // and show errors.
4149 $mform->is_validated();
4152 $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
4154 if (count($allsubmissions) > 1) {
4155 $allgrades = $this->get_all_grades($userid);
4156 $history = new assign_attempt_history_chooser($allsubmissions,
4157 $allgrades,
4158 $this->get_course_module()->id,
4159 $userid);
4161 $o .= $this->get_renderer()->render($history);
4164 \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
4166 return $o;
4170 * Print the grading page for a single user submission.
4172 * @param moodleform $mform
4173 * @return string
4175 protected function view_single_grade_page($mform) {
4176 global $DB, $CFG, $SESSION;
4178 $o = '';
4179 $instance = $this->get_instance();
4181 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
4183 // Need submit permission to submit an assignment.
4184 require_capability('mod/assign:grade', $this->context);
4186 $header = new assign_header($instance,
4187 $this->get_context(),
4188 false,
4189 $this->get_course_module()->id,
4190 get_string('grading', 'assign'));
4191 $o .= $this->get_renderer()->render($header);
4193 // If userid is passed - we are only grading a single student.
4194 $rownum = optional_param('rownum', 0, PARAM_INT);
4195 $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
4196 $userid = optional_param('userid', 0, PARAM_INT);
4197 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
4199 if (!$userid) {
4200 $useridlist = $this->get_grading_userid_list(true, $useridlistid);
4201 } else {
4202 $rownum = 0;
4203 $useridlistid = 0;
4204 $useridlist = array($userid);
4207 if ($rownum < 0 || $rownum > count($useridlist)) {
4208 throw new coding_exception('Row is out of bounds for the current grading table: ' . $rownum);
4211 $last = false;
4212 $userid = $useridlist[$rownum];
4213 if ($rownum == count($useridlist) - 1) {
4214 $last = true;
4216 // This variation on the url will link direct to this student, with no next/previous links.
4217 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4218 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4219 $this->register_return_link('grade', $returnparams);
4221 $user = $DB->get_record('user', array('id' => $userid));
4222 if ($user) {
4223 $this->update_effective_access($userid);
4224 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4225 $usersummary = new assign_user_summary($user,
4226 $this->get_course()->id,
4227 $viewfullnames,
4228 $this->is_blind_marking(),
4229 $this->get_uniqueid_for_user($user->id),
4230 // TODO Does not support custom user profile fields (MDL-70456).
4231 \core_user\fields::get_identity_fields($this->get_context(), false),
4232 !$this->is_active_user($userid));
4233 $o .= $this->get_renderer()->render($usersummary);
4235 $submission = $this->get_user_submission($userid, false, $attemptnumber);
4236 $submissiongroup = null;
4237 $teamsubmission = null;
4238 $notsubmitted = array();
4239 if ($instance->teamsubmission) {
4240 $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
4241 $submissiongroup = $this->get_submission_group($userid);
4242 $groupid = 0;
4243 if ($submissiongroup) {
4244 $groupid = $submissiongroup->id;
4246 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
4250 // Get the requested grade.
4251 $grade = $this->get_user_grade($userid, false, $attemptnumber);
4252 $flags = $this->get_user_flags($userid, false);
4253 if ($this->can_view_submission($userid)) {
4254 $submissionlocked = ($flags && $flags->locked);
4255 $extensionduedate = null;
4256 if ($flags) {
4257 $extensionduedate = $flags->extensionduedate;
4259 $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
4260 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4261 $usergroups = $this->get_all_groups($user->id);
4262 $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
4263 $instance->alwaysshowdescription,
4264 $submission,
4265 $instance->teamsubmission,
4266 $teamsubmission,
4267 $submissiongroup,
4268 $notsubmitted,
4269 $this->is_any_submission_plugin_enabled(),
4270 $submissionlocked,
4271 $this->is_graded($userid),
4272 $instance->duedate,
4273 $instance->cutoffdate,
4274 $this->get_submission_plugins(),
4275 $this->get_return_action(),
4276 $this->get_return_params(),
4277 $this->get_course_module()->id,
4278 $this->get_course()->id,
4279 assign_submission_status::GRADER_VIEW,
4280 $showedit,
4281 false,
4282 $viewfullnames,
4283 $extensionduedate,
4284 $this->get_context(),
4285 $this->is_blind_marking(),
4287 $instance->attemptreopenmethod,
4288 $instance->maxattempts,
4289 $this->get_grading_status($userid),
4290 $instance->preventsubmissionnotingroup,
4291 $usergroups,
4292 $instance->timelimit);
4293 $o .= $this->get_renderer()->render($submissionstatus);
4296 if ($grade) {
4297 $data = new stdClass();
4298 if ($grade->grade !== null && $grade->grade >= 0) {
4299 $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
4301 } else {
4302 $data = new stdClass();
4303 $data->grade = '';
4306 if (!empty($flags->workflowstate)) {
4307 $data->workflowstate = $flags->workflowstate;
4309 if (!empty($flags->allocatedmarker)) {
4310 $data->allocatedmarker = $flags->allocatedmarker;
4313 // Warning if required.
4314 $allsubmissions = $this->get_all_submissions($userid);
4316 if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
4317 $params = array('attemptnumber'=>$attemptnumber + 1,
4318 'totalattempts'=>count($allsubmissions));
4319 $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
4320 $o .= $this->get_renderer()->notification($message);
4323 // Now show the grading form.
4324 if (!$mform) {
4325 $pagination = array('rownum' => $rownum,
4326 'useridlistid' => $useridlistid,
4327 'last' => $last,
4328 'userid' => $userid,
4329 'attemptnumber' => $attemptnumber);
4330 $formparams = array($this, $data, $pagination);
4331 $mform = new mod_assign_grade_form(null,
4332 $formparams,
4333 'post',
4335 array('class'=>'gradeform'));
4338 $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
4340 if (count($allsubmissions) > 1 && $attemptnumber == -1) {
4341 $allgrades = $this->get_all_grades($userid);
4342 $history = new assign_attempt_history($allsubmissions,
4343 $allgrades,
4344 $this->get_submission_plugins(),
4345 $this->get_feedback_plugins(),
4346 $this->get_course_module()->id,
4347 $this->get_return_action(),
4348 $this->get_return_params(),
4349 true,
4350 $useridlistid,
4351 $rownum);
4353 $o .= $this->get_renderer()->render($history);
4356 \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
4358 $o .= $this->view_footer();
4359 return $o;
4363 * Show a confirmation page to make sure they want to remove submission data.
4365 * @return string
4367 protected function view_remove_submission_confirm() {
4368 global $USER;
4370 $userid = optional_param('userid', $USER->id, PARAM_INT);
4372 if (!$this->can_edit_submission($userid, $USER->id)) {
4373 throw new \moodle_exception('nopermission');
4375 $user = core_user::get_user($userid, '*', MUST_EXIST);
4377 $o = '';
4378 $header = new assign_header($this->get_instance(),
4379 $this->get_context(),
4380 false,
4381 $this->get_course_module()->id);
4382 $o .= $this->get_renderer()->render($header);
4384 $urlparams = array('id' => $this->get_course_module()->id,
4385 'action' => 'removesubmission',
4386 'userid' => $userid,
4387 'sesskey' => sesskey());
4388 $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
4390 $urlparams = array('id' => $this->get_course_module()->id,
4391 'action' => 'view');
4392 $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
4394 if ($userid == $USER->id) {
4395 if ($this->is_time_limit_enabled($userid)) {
4396 $confirmstr = get_string('removesubmissionconfirmwithtimelimit', 'assign');
4397 } else {
4398 $confirmstr = get_string('removesubmissionconfirm', 'assign');
4400 } else {
4401 if ($this->is_time_limit_enabled($userid)) {
4402 $confirmstr = get_string('removesubmissionconfirmforstudentwithtimelimit', 'assign', $this->fullname($user));
4403 } else {
4404 $confirmstr = get_string('removesubmissionconfirmforstudent', 'assign', $this->fullname($user));
4407 $o .= $this->get_renderer()->confirm($confirmstr,
4408 $confirmurl,
4409 $cancelurl);
4410 $o .= $this->view_footer();
4412 \mod_assign\event\remove_submission_form_viewed::create_from_user($this, $user)->trigger();
4414 return $o;
4419 * Show a confirmation page to make sure they want to release student identities.
4421 * @return string
4423 protected function view_reveal_identities_confirm() {
4424 require_capability('mod/assign:revealidentities', $this->get_context());
4426 $o = '';
4427 $header = new assign_header($this->get_instance(),
4428 $this->get_context(),
4429 false,
4430 $this->get_course_module()->id);
4431 $o .= $this->get_renderer()->render($header);
4433 $urlparams = array('id'=>$this->get_course_module()->id,
4434 'action'=>'revealidentitiesconfirm',
4435 'sesskey'=>sesskey());
4436 $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
4438 $urlparams = array('id'=>$this->get_course_module()->id,
4439 'action'=>'grading');
4440 $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
4442 $o .= $this->get_renderer()->confirm(get_string('revealidentitiesconfirm', 'assign'),
4443 $confirmurl,
4444 $cancelurl);
4445 $o .= $this->view_footer();
4447 \mod_assign\event\reveal_identities_confirmation_page_viewed::create_from_assign($this)->trigger();
4449 return $o;
4453 * View a link to go back to the previous page. Uses url parameters returnaction and returnparams.
4455 * @return string
4457 protected function view_return_links() {
4458 $returnaction = optional_param('returnaction', '', PARAM_ALPHA);
4459 $returnparams = optional_param('returnparams', '', PARAM_TEXT);
4461 $params = array();
4462 $returnparams = str_replace('&amp;', '&', $returnparams);
4463 parse_str($returnparams, $params);
4464 $newparams = array('id' => $this->get_course_module()->id, 'action' => $returnaction);
4465 $params = array_merge($newparams, $params);
4467 $url = new moodle_url('/mod/assign/view.php', $params);
4468 return $this->get_renderer()->single_button($url, get_string('back'), 'get');
4472 * View the grading table of all submissions for this assignment.
4474 * @return string
4476 protected function view_grading_table() {
4477 global $USER, $CFG, $SESSION, $PAGE, $OUTPUT;
4479 require_once($CFG->dirroot . '/mod/assign/quickgradingform.php');
4481 $submittedfilter = optional_param('status', null, PARAM_ALPHA);
4482 if (isset($submittedfilter)) {
4483 $validfilters = array_column($this->get_filters(), 'key');
4484 $validfilters = array_diff($validfilters, [ASSIGN_FILTER_NONE]); // The 'none' filter is not a real filter.
4485 if ($submittedfilter === '' || in_array($submittedfilter, $validfilters)) {
4486 set_user_preference('assign_filter', $submittedfilter);
4490 $submittedquickgrading = optional_param('quickgrading', null, PARAM_BOOL);
4491 if (isset($submittedquickgrading)) {
4492 set_user_preference('assign_quickgrading', $submittedquickgrading);
4495 $submitteddownloadasfolders = optional_param('downloadasfolders', null, PARAM_BOOL);
4496 if (isset($submitteddownloadasfolders)) {
4497 set_user_preference('assign_downloadasfolders', $submitteddownloadasfolders);
4500 $submittedperpage = optional_param('perpage', null, PARAM_INT);
4501 if (isset($submittedperpage)) {
4502 set_user_preference('assign_perpage', $submittedperpage);
4505 $o = '';
4506 $cmid = $this->get_course_module()->id;
4508 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
4510 $perpage = $this->get_assign_perpage();
4511 $filter = get_user_preferences('assign_filter', '');
4513 // Retrieve the 'workflowfilter' parameter, or set it to null if not provided.
4514 $workflowfilter = optional_param('workflowfilter', null, PARAM_ALPHA);
4515 // Check if the parameter is not null and if it exists in the list of valid workflow filters.
4516 if ($workflowfilter !== null && array_key_exists($workflowfilter, $this->get_marking_workflow_filters())) {
4517 // Save the valid 'workflowfilter' value as a user preference.
4518 set_user_preference('assign_workflowfilter', $workflowfilter);
4521 // Retrieve the 'markingallocationfilter' parameter, or set it to null if not provided.
4522 $markingallocationfilter = optional_param('markingallocationfilter', null, PARAM_ALPHANUMEXT);
4523 // Check if the parameter is not null and if it exists in the list of valid marking allocation filters.
4524 if ($markingallocationfilter !== null &&
4525 array_key_exists($markingallocationfilter, $this->get_marking_allocation_filters())) {
4526 // Save the valid 'markingallocationfilter' value as a user preference.
4527 set_user_preference('assign_markerfilter', $markingallocationfilter);
4530 // Retrieve the 'suspendedparticipantsfilter' parameter, or set it to null if not provided.
4531 $suspendedparticipantsfilter = optional_param('suspendedparticipantsfilter', null, PARAM_BOOL);
4532 if ($suspendedparticipantsfilter !== null &&
4533 has_capability('moodle/course:viewsuspendedusers', $this->get_context())) {
4534 // Save the 'suspendedparticipantsfilter' value as a user preference.
4535 set_user_preference('grade_report_showonlyactiveenrol', !$suspendedparticipantsfilter);
4538 $controller = $gradingmanager->get_active_controller();
4539 $showquickgrading = empty($controller) && $this->can_grade();
4540 $quickgrading = get_user_preferences('assign_quickgrading', false);
4542 $markingallocation = $this->get_instance()->markingworkflow &&
4543 $this->get_instance()->markingallocation &&
4544 has_capability('mod/assign:manageallocations', $this->context);
4546 $markingworkflow = $this->get_instance()->markingworkflow;
4548 // Load the table of submissions, to be printed further down.
4549 $usequickgrading = $showquickgrading && $quickgrading;
4550 $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, $usequickgrading);
4551 $gradingtable->responsive = false;
4552 $table = $this->get_renderer()->render($gradingtable);
4553 // This initialises the selected first/last initials for the action menu.
4554 $gradingtable->initialbars(true);
4555 $buttons = new \mod_assign\output\grading_actionmenu(
4556 cmid: $this->get_course_module()->id,
4557 assign: $this,
4558 userinitials: [
4559 'firstname' => $gradingtable->get_initial_first(),
4560 'lastname' => $gradingtable->get_initial_last(),
4563 $actionformtext = $this->get_renderer()->render($buttons);
4564 $currenturl = new moodle_url('/mod/assign/view.php', ['id' => $this->get_course_module()->id, 'action' => 'grading']);
4565 $PAGE->activityheader->set_attrs(['hidecompletion' => true]);
4567 $PAGE->requires->js_call_amd('mod_assign/user', 'init', [$currenturl->out(false)]);
4569 // Conditionally add the group JS if we have groups enabled.
4570 if (groups_get_activity_groupmode($this->get_course_module(), $this->get_course())) {
4571 $PAGE->requires->js_call_amd('core_course/actionbar/group', 'init', [$currenturl->out(false), $cmid]);
4574 $header = new assign_header($this->get_instance(),
4575 $this->get_context(),
4576 false,
4577 $this->get_course_module()->id,
4578 get_string('gradeitem:submissions', 'assign'),
4581 $currenturl);
4582 $o .= $this->get_renderer()->render($header);
4584 $o .= $actionformtext;
4586 if ($this->is_blind_marking() && has_capability('mod/assign:viewblinddetails', $this->get_context())) {
4587 $o .= $this->get_renderer()->notification(get_string('blindmarkingenabledwarning', 'assign'), 'notifymessage');
4590 // Print the table of submissions.
4591 $footerdata = [
4592 'perpage' => $gradingtable->get_paging_selector(),
4593 'pagingbar' => $gradingtable->get_paging_bar(),
4594 'hassubmit' => $usequickgrading,
4595 'sendstudentnotifications' => $this->get_instance()->sendstudentnotifications,
4597 $footer = new core\output\sticky_footer($OUTPUT->render_from_template('mod_assign/grading_sticky_footer', $footerdata));
4599 if ($usequickgrading) {
4600 $page = optional_param('page', null, PARAM_INT);
4601 $PAGE->requires->js_call_amd('mod_assign/quick_grading', 'init', []);
4602 $quickformparams = [
4603 'cm' => $this->get_course_module()->id,
4604 'gradingtable' => $table,
4605 'page' => $page,
4606 'footer' => $this->get_renderer()->render($footer),
4608 $quickgradingform = new mod_assign_quick_grading_form(null, $quickformparams);
4610 $o .= $this->get_renderer()->render(new assign_form('quickgradingform', $quickgradingform));
4611 } else {
4612 $o .= $table;
4613 $o .= $this->get_renderer()->render($footer);
4616 if ($this->can_grade()) {
4617 // We need to store the order of uses in the table as the person may wish to grade them.
4618 // This is done based on the row number of the user.
4619 // Pagination has be added before this because calling get_column_data will reset the pagination.
4620 $useridlist = $gradingtable->get_column_data('userid');
4621 $SESSION->mod_assign_useridlist[$this->get_useridlist_key()] = $useridlist;
4624 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4625 $users = array_keys($this->list_participants($currentgroup, true));
4626 if (count($users) != 0 && $this->can_grade()) {
4627 $jsparams = [];
4628 $jsparams['message'] = !empty($CFG->messaging)
4629 && has_all_capabilities(['moodle/site:sendmessage', 'moodle/course:bulkmessaging'], $this->context);
4630 $jsparams['submissiondrafts'] = !empty($this->get_instance()->submissiondrafts);
4631 $jsparams['removesubmission'] = has_capability('mod/assign:editothersubmission', $this->context);
4632 $jsparams['extend'] = $this->get_instance()->duedate && has_capability('mod/assign:grantextension', $this->context);
4634 $multipleattemptsallowed = $this->get_instance()->maxattempts > 1
4635 || $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS;
4636 $jsparams['grantattempt'] =
4637 $multipleattemptsallowed && $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL;
4639 $jsparams['pluginoperations'] = [];
4640 foreach ($this->get_feedback_plugins() as $plugin) {
4641 if ($plugin->is_visible() && $plugin->is_enabled()) {
4642 foreach ($plugin->get_grading_batch_operation_details() as $operation) {
4643 $jsparams['pluginoperations'][] = [
4644 'key' => 'plugingradingbatchoperation_' . $plugin->get_type() . '_' . $operation->key,
4645 'label' => $operation->label,
4646 'icon' => $operation->icon,
4647 'confirmationtitle' => $operation->confirmationtitle,
4648 'confirmationquestion' => $operation->confirmationquestion,
4654 $jsparams['workflowstate'] = !empty($this->get_instance()->markingworkflow);
4655 $jsparams['markingallocation'] = !empty($this->get_instance()->markingallocation);
4656 $jsparams['cmid'] = $this->get_course_module()->id;
4657 $jsparams['sesskey'] = sesskey();
4659 $PAGE->requires->js_call_amd('mod_assign/bulkactions/grading/bulk_actions', 'init', [$jsparams]);
4662 return $o;
4666 * View entire grader app.
4668 * @return string
4670 protected function view_grader() {
4671 global $USER, $PAGE;
4673 $o = '';
4674 // Need submit permission to submit an assignment.
4675 $this->require_view_grades();
4677 $PAGE->set_pagelayout('embedded');
4679 $PAGE->activityheader->disable();
4681 $courseshortname = $this->get_context()->get_course_context()->get_context_name(false, true);
4682 $args = [
4683 'contextname' => $this->get_context()->get_context_name(false, true),
4684 'subpage' => get_string('grading', 'assign')
4686 $title = get_string('subpagetitle', 'assign', $args);
4687 $title = $courseshortname . ': ' . $title;
4688 $PAGE->set_title($title);
4690 $o .= $this->get_renderer()->header();
4692 $userid = optional_param('userid', 0, PARAM_INT);
4693 $blindid = optional_param('blindid', 0, PARAM_INT);
4695 if (!$userid && $blindid) {
4696 $userid = $this->get_user_id_for_uniqueid($blindid);
4699 // Instantiate table object to apply table preferences.
4700 $gradingtable = new assign_grading_table($this, 10, '', 0, false);
4701 $gradingtable->setup();
4703 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4704 $framegrader = new grading_app($userid, $currentgroup, $this);
4706 $this->update_effective_access($userid);
4708 $o .= $this->get_renderer()->render($framegrader);
4710 $o .= $this->view_footer();
4712 \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4714 return $o;
4717 * View entire grading page.
4719 * @return string
4721 protected function view_grading_page() {
4722 global $CFG, $PAGE;
4724 // Ensure that the 'Submissions' navigation node is highlighted as 'active' in the secondary navigation.
4725 $PAGE->set_secondary_active_tab('mod_assign_submissions');
4727 $o = '';
4728 // Need submit permission to submit an assignment.
4729 $this->require_view_grades();
4730 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
4732 $this->add_grade_notices();
4734 // Only load this if it is.
4735 $o .= $this->view_grading_table();
4737 $o .= $this->view_footer();
4739 \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4741 return $o;
4745 * Capture the output of the plagiarism plugins disclosures and return it as a string.
4747 * @return string
4749 protected function plagiarism_print_disclosure() {
4750 global $CFG;
4751 $o = '';
4753 if (!empty($CFG->enableplagiarism)) {
4754 require_once($CFG->libdir . '/plagiarismlib.php');
4756 $o .= plagiarism_print_disclosure($this->get_course_module()->id);
4759 return $o;
4763 * Message for students when assignment submissions have been closed.
4765 * @param string $title The page title
4766 * @param array $notices The array of notices to show.
4767 * @return string
4769 protected function view_notices($title, $notices) {
4770 global $CFG;
4772 $o = '';
4774 $header = new assign_header($this->get_instance(),
4775 $this->get_context(),
4776 $this->show_intro(),
4777 $this->get_course_module()->id,
4778 $title);
4779 $o .= $this->get_renderer()->render($header);
4781 foreach ($notices as $notice) {
4782 $o .= $this->get_renderer()->notification($notice);
4785 $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id, 'action'=>'view'));
4786 $o .= $this->get_renderer()->continue_button($url);
4788 $o .= $this->view_footer();
4790 return $o;
4794 * Get the name for a user - hiding their real name if blind marking is on.
4796 * @param stdClass $user The user record as required by fullname()
4797 * @return string The name.
4799 public function fullname($user) {
4800 if ($this->is_blind_marking()) {
4801 $hasviewblind = has_capability('mod/assign:viewblinddetails', $this->get_context());
4802 if (empty($user->recordid)) {
4803 $uniqueid = $this->get_uniqueid_for_user($user->id);
4804 } else {
4805 $uniqueid = $user->recordid;
4807 if ($hasviewblind) {
4808 return get_string('participant', 'assign') . ' ' . $uniqueid . ' (' .
4809 fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())) . ')';
4810 } else {
4811 return get_string('participant', 'assign') . ' ' . $uniqueid;
4813 } else {
4814 return fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context()));
4819 * View edit submissions page.
4821 * @param moodleform $mform
4822 * @param array $notices A list of notices to display at the top of the
4823 * edit submission form (e.g. from plugins).
4824 * @return string The page output.
4826 protected function view_edit_submission_page($mform, $notices) {
4827 global $CFG, $USER, $DB, $PAGE;
4829 $o = '';
4830 require_once($CFG->dirroot . '/mod/assign/submission_form.php');
4831 // Need submit permission to submit an assignment.
4832 $userid = optional_param('userid', $USER->id, PARAM_INT);
4833 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
4834 $timelimitenabled = get_config('assign', 'enabletimelimit');
4836 // This variation on the url will link direct to this student.
4837 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4838 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4839 $this->register_return_link('editsubmission', $returnparams);
4841 if ($userid == $USER->id) {
4842 if (!$this->can_edit_submission($userid, $USER->id)) {
4843 throw new \moodle_exception('nopermission');
4845 // User is editing their own submission.
4846 require_capability('mod/assign:submit', $this->context);
4847 $title = get_string('editsubmission', 'assign');
4848 } else {
4849 // User is editing another user's submission.
4850 if (!$this->can_edit_submission($userid, $USER->id)) {
4851 throw new \moodle_exception('nopermission');
4854 $name = $this->fullname($user);
4855 $title = get_string('editsubmissionother', 'assign', $name);
4858 if (!$this->submissions_open($userid)) {
4859 $message = array(get_string('submissionsclosed', 'assign'));
4860 return $this->view_notices($title, $message);
4863 $postfix = '';
4864 if ($this->has_visible_attachments()) {
4865 $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
4868 $data = new stdClass();
4869 $data->userid = $userid;
4870 if (!$mform) {
4871 $mform = new mod_assign_submission_form(null, array($this, $data));
4874 if ($this->get_instance()->teamsubmission) {
4875 $submission = $this->get_group_submission($userid, 0, false);
4876 } else {
4877 $submission = $this->get_user_submission($userid, false);
4880 if ($timelimitenabled && !empty($submission->timestarted) && $this->get_instance()->timelimit) {
4881 $navbc = $this->get_timelimit_panel($submission);
4882 $regions = $PAGE->blocks->get_regions();
4883 $bc = new \block_contents();
4884 $bc->attributes['id'] = 'mod_assign_timelimit_block';
4885 $bc->attributes['role'] = 'navigation';
4886 $bc->attributes['aria-labelledby'] = 'mod_assign_timelimit_block_title';
4887 $bc->title = get_string('assigntimeleft', 'assign');
4888 $bc->content = $navbc;
4889 $PAGE->blocks->add_fake_block($bc, reset($regions));
4892 $o .= $this->get_renderer()->render(
4893 new assign_header($this->get_instance(),
4894 $this->get_context(),
4895 $this->show_intro(),
4896 $this->get_course_module()->id,
4897 $title,
4899 $postfix,
4900 null,
4901 true
4905 // Show plagiarism disclosure for any user submitter.
4906 $o .= $this->plagiarism_print_disclosure();
4908 foreach ($notices as $notice) {
4909 $o .= $this->get_renderer()->notification($notice);
4912 $o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
4913 $o .= $this->view_footer();
4915 \mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger();
4917 return $o;
4921 * Get the time limit panel object for this submission attempt.
4923 * @param stdClass $submission assign submission.
4924 * @return string the panel output.
4926 public function get_timelimit_panel(stdClass $submission): string {
4927 global $USER;
4929 // Apply overrides.
4930 $this->update_effective_access($USER->id);
4931 $panel = new timelimit_panel($submission, $this->get_instance());
4932 return $this->get_renderer()->render($panel);
4936 * See if this assignment has a grade yet.
4938 * @param int $userid
4939 * @return bool
4941 protected function is_graded($userid) {
4942 $grade = $this->get_user_grade($userid, false);
4943 if ($grade) {
4944 return ($grade->grade !== null && $grade->grade >= 0);
4946 return false;
4950 * Perform an access check to see if the current $USER can edit this group submission.
4952 * @param int $groupid
4953 * @return bool
4955 public function can_edit_group_submission($groupid) {
4956 global $USER;
4958 $members = $this->get_submission_group_members($groupid, true);
4959 foreach ($members as $member) {
4960 // If we can edit any members submission, we can edit the submission for the group.
4961 if ($this->can_edit_submission($member->id)) {
4962 return true;
4965 return false;
4969 * Perform an access check to see if the current $USER can view this group submission.
4971 * @param int $groupid
4972 * @return bool
4974 public function can_view_group_submission($groupid) {
4975 global $USER;
4977 $members = $this->get_submission_group_members($groupid, true);
4978 foreach ($members as $member) {
4979 // If we can view any members submission, we can view the submission for the group.
4980 if ($this->can_view_submission($member->id)) {
4981 return true;
4984 return false;
4988 * Perform an access check to see if the current $USER can view this users submission.
4990 * @param int $userid
4991 * @return bool
4993 public function can_view_submission($userid) {
4994 global $USER;
4996 if (!$this->is_active_user($userid) && !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
4997 return false;
4999 if (!is_enrolled($this->get_course_context(), $userid)) {
5000 return false;
5002 if (has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
5003 return true;
5005 if ($userid == $USER->id) {
5006 return true;
5008 return false;
5012 * Allows the plugin to show a batch grading operation page.
5013 * You should confirm sesskey before calling this function.
5015 * @return none
5017 protected function view_plugin_grading_batch_operation() {
5018 require_capability('mod/assign:grade', $this->context);
5019 $prefix = 'plugingradingbatchoperation_';
5021 $operation = required_param('operation', PARAM_ALPHAEXT);
5023 $tail = substr($operation, strlen($prefix));
5024 list($plugintype, $action) = explode('_', $tail, 2);
5026 $plugin = $this->get_feedback_plugin_by_type($plugintype);
5027 if ($plugin) {
5028 $users = required_param('selectedusers', PARAM_SEQUENCE);
5029 $userlist = explode(',', $users);
5030 echo $plugin->grading_batch_operation($action, $userlist);
5031 return;
5034 throw new \moodle_exception('invalidformdata', '');
5038 * Ask the user to confirm they want to perform this batch operation
5040 * @return string - the page to view after processing these actions
5042 protected function process_grading_batch_operation() {
5043 require_sesskey();
5045 $operation = required_param('operation', PARAM_ALPHAEXT);
5046 $selectedusers = required_param('selectedusers', PARAM_SEQUENCE);
5048 // Get the list of users.
5049 $userlist = explode(',', $selectedusers);
5051 $prefix = 'plugingradingbatchoperation_';
5053 if ($operation == 'grantextension') {
5054 return 'grantextension';
5055 } else if ($operation == 'setmarkingworkflowstate') {
5056 return 'viewbatchsetmarkingworkflowstate';
5057 } else if ($operation == 'setmarkingallocation') {
5058 return 'viewbatchmarkingallocation';
5059 } else if (strpos($operation, $prefix) === 0) {
5060 $tail = substr($operation, strlen($prefix));
5061 list($plugintype, $action) = explode('_', $tail, 2);
5063 $plugin = $this->get_feedback_plugin_by_type($plugintype);
5064 if ($plugin) {
5065 return 'plugingradingbatchoperation';
5069 if ($operation == 'downloadselected') {
5070 $this->download_submissions($userlist);
5071 } else {
5072 foreach ($userlist as $userid) {
5073 if ($operation == 'lock') {
5074 $this->process_lock_submission($userid);
5075 } else if ($operation == 'unlock') {
5076 $this->process_unlock_submission($userid);
5077 } else if ($operation == 'reverttodraft') {
5078 $this->process_revert_to_draft($userid);
5079 } else if ($operation == 'removesubmission') {
5080 $this->process_remove_submission($userid);
5081 } else if ($operation == 'addattempt') {
5082 if (!$this->get_instance()->teamsubmission) {
5083 $this->process_add_attempt($userid);
5088 if ($this->get_instance()->teamsubmission && $operation == 'addattempt') {
5089 // This needs to be handled separately so that each team submission is only re-opened one time.
5090 $this->process_add_attempt_group($userlist);
5093 return 'grading';
5097 * Shows a form that allows the workflow state for selected submissions to be changed.
5098 * You should confirm sesskey before calling this function.
5100 * @return string - the page to view after processing these actions
5102 protected function view_batch_set_workflow_state() {
5103 global $CFG, $DB;
5105 require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
5107 $o = '';
5109 $users = required_param('selectedusers', PARAM_SEQUENCE);
5110 $userlist = explode(',', $users);
5112 $formdata = array('id' => $this->get_course_module()->id,
5113 'selectedusers' => $users);
5115 $usershtml = '';
5117 $usercount = 0;
5118 // TODO Does not support custom user profile fields (MDL-70456).
5119 $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false);
5120 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5121 foreach ($userlist as $userid) {
5122 if ($usercount >= 5) {
5123 $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
5124 break;
5126 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
5128 $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
5129 $this->get_course()->id,
5130 $viewfullnames,
5131 $this->is_blind_marking(),
5132 $this->get_uniqueid_for_user($user->id),
5133 $extrauserfields,
5134 !$this->is_active_user($userid)));
5135 $usercount += 1;
5138 $formparams = array(
5139 'userscount' => count($userlist),
5140 'usershtml' => $usershtml,
5141 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
5144 $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
5145 $mform->set_data($formdata); // Initialises the hidden elements.
5146 $header = new assign_header($this->get_instance(),
5147 $this->get_context(),
5148 $this->show_intro(),
5149 $this->get_course_module()->id,
5150 get_string('setmarkingworkflowstate', 'assign'));
5151 $o .= $this->get_renderer()->render($header);
5152 $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
5153 $o .= $this->view_footer();
5155 \mod_assign\event\batch_set_workflow_state_viewed::create_from_assign($this)->trigger();
5157 return $o;
5161 * Shows a form that allows the allocated marker for selected submissions to be changed.
5162 * You should confirm sesskey before calling this function.
5164 * @return string - the page to view after processing these actions
5166 public function view_batch_markingallocation() {
5167 global $CFG, $DB;
5169 require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
5171 $o = '';
5173 $users = required_param('selectedusers', PARAM_SEQUENCE);
5174 $userlist = explode(',', $users);
5176 $formdata = array('id' => $this->get_course_module()->id,
5177 'selectedusers' => $users);
5179 $usershtml = '';
5181 $usercount = 0;
5182 // TODO Does not support custom user profile fields (MDL-70456).
5183 $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false);
5184 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5185 foreach ($userlist as $userid) {
5186 if ($usercount >= 5) {
5187 $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
5188 break;
5190 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
5192 $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
5193 $this->get_course()->id,
5194 $viewfullnames,
5195 $this->is_blind_marking(),
5196 $this->get_uniqueid_for_user($user->id),
5197 $extrauserfields,
5198 !$this->is_active_user($userid)));
5199 $usercount += 1;
5202 $formparams = array(
5203 'userscount' => count($userlist),
5204 'usershtml' => $usershtml,
5207 list($sort, $params) = users_order_by_sql('u');
5208 // Only enrolled users could be assigned as potential markers.
5209 $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
5210 $markerlist = array();
5211 foreach ($markers as $marker) {
5212 $markerlist[$marker->id] = fullname($marker);
5215 $formparams['markers'] = $markerlist;
5217 $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
5218 $mform->set_data($formdata); // Initialises the hidden elements.
5219 $header = new assign_header($this->get_instance(),
5220 $this->get_context(),
5221 $this->show_intro(),
5222 $this->get_course_module()->id,
5223 get_string('setmarkingallocation', 'assign'));
5224 $o .= $this->get_renderer()->render($header);
5225 $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
5226 $o .= $this->view_footer();
5228 \mod_assign\event\batch_set_marker_allocation_viewed::create_from_assign($this)->trigger();
5230 return $o;
5234 * Ask the user to confirm they want to submit their work for grading.
5236 * @param moodleform $mform - null unless form validation has failed
5237 * @return string
5239 protected function check_submit_for_grading($mform) {
5240 global $USER, $CFG;
5242 require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
5244 // Check that all of the submission plugins are ready for this submission.
5245 // Also check whether there is something to be submitted as well against atleast one.
5246 $notifications = array();
5247 $submission = $this->get_user_submission($USER->id, false);
5248 if ($this->get_instance()->teamsubmission) {
5249 $submission = $this->get_group_submission($USER->id, 0, false);
5252 $plugins = $this->get_submission_plugins();
5253 $hassubmission = false;
5254 foreach ($plugins as $plugin) {
5255 if ($plugin->is_enabled() && $plugin->is_visible()) {
5256 $check = $plugin->precheck_submission($submission);
5257 if ($check !== true) {
5258 $notifications[] = $check;
5261 if (is_object($submission) && !$plugin->is_empty($submission)) {
5262 $hassubmission = true;
5267 // If there are no submissions and no existing notifications to be displayed the stop.
5268 if (!$hassubmission && !$notifications) {
5269 $notifications[] = get_string('addsubmission_help', 'assign');
5272 $data = new stdClass();
5273 $adminconfig = $this->get_admin_config();
5274 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
5275 $submissionstatement = '';
5277 if ($requiresubmissionstatement) {
5278 $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
5281 // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
5282 // that the submission statement checkbox will be displayed.
5283 if (empty($submissionstatement)) {
5284 $requiresubmissionstatement = false;
5287 if ($mform == null) {
5288 $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
5289 $submissionstatement,
5290 $this->get_course_module()->id,
5291 $data));
5293 $o = '';
5294 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
5295 $this->get_context(),
5296 $this->show_intro(),
5297 $this->get_course_module()->id,
5298 get_string('confirmsubmissionheading', 'assign')));
5299 $submitforgradingpage = new assign_submit_for_grading_page($notifications,
5300 $this->get_course_module()->id,
5301 $mform);
5302 $o .= $this->get_renderer()->render($submitforgradingpage);
5303 $o .= $this->view_footer();
5305 \mod_assign\event\submission_confirmation_form_viewed::create_from_assign($this)->trigger();
5307 return $o;
5311 * Creates an assign_submission_status renderable.
5313 * @param stdClass $user the user to get the report for
5314 * @param bool $showlinks return plain text or links to the profile
5315 * @return assign_submission_status renderable object
5317 public function get_assign_submission_status_renderable($user, $showlinks) {
5318 global $PAGE;
5320 $instance = $this->get_instance();
5321 $flags = $this->get_user_flags($user->id, false);
5322 $submission = $this->get_user_submission($user->id, false);
5324 $teamsubmission = null;
5325 $submissiongroup = null;
5326 $notsubmitted = array();
5327 if ($instance->teamsubmission) {
5328 $teamsubmission = $this->get_group_submission($user->id, 0, false);
5329 $submissiongroup = $this->get_submission_group($user->id);
5330 $groupid = 0;
5331 if ($submissiongroup) {
5332 $groupid = $submissiongroup->id;
5334 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
5337 $showedit = $showlinks &&
5338 ($this->is_any_submission_plugin_enabled()) &&
5339 $this->can_edit_submission($user->id);
5341 $submissionlocked = ($flags && $flags->locked);
5343 // Grading criteria preview.
5344 $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
5345 $gradingcontrollerpreview = '';
5346 if ($gradingmethod = $gradingmanager->get_active_method()) {
5347 $controller = $gradingmanager->get_controller($gradingmethod);
5348 if ($controller->is_form_defined()) {
5349 $gradingcontrollerpreview = $controller->render_preview($PAGE);
5353 $showsubmit = ($showlinks && $this->submissions_open($user->id));
5354 $showsubmit = ($showsubmit && $this->show_submit_button($submission, $teamsubmission, $user->id));
5356 $extensionduedate = null;
5357 if ($flags) {
5358 $extensionduedate = $flags->extensionduedate;
5360 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5362 $gradingstatus = $this->get_grading_status($user->id);
5363 $usergroups = $this->get_all_groups($user->id);
5364 $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
5365 $instance->alwaysshowdescription,
5366 $submission,
5367 $instance->teamsubmission,
5368 $teamsubmission,
5369 $submissiongroup,
5370 $notsubmitted,
5371 $this->is_any_submission_plugin_enabled(),
5372 $submissionlocked,
5373 $this->is_graded($user->id),
5374 $instance->duedate,
5375 $instance->cutoffdate,
5376 $this->get_submission_plugins(),
5377 $this->get_return_action(),
5378 $this->get_return_params(),
5379 $this->get_course_module()->id,
5380 $this->get_course()->id,
5381 assign_submission_status::STUDENT_VIEW,
5382 $showedit,
5383 $showsubmit,
5384 $viewfullnames,
5385 $extensionduedate,
5386 $this->get_context(),
5387 $this->is_blind_marking(),
5388 $gradingcontrollerpreview,
5389 $instance->attemptreopenmethod,
5390 $instance->maxattempts,
5391 $gradingstatus,
5392 $instance->preventsubmissionnotingroup,
5393 $usergroups,
5394 $instance->timelimit);
5395 return $submissionstatus;
5400 * Creates an assign_feedback_status renderable.
5402 * @param stdClass $user the user to get the report for
5403 * @return assign_feedback_status renderable object
5405 public function get_assign_feedback_status_renderable($user) {
5406 global $CFG, $DB, $PAGE;
5408 require_once($CFG->libdir.'/gradelib.php');
5409 require_once($CFG->dirroot.'/grade/grading/lib.php');
5411 $instance = $this->get_instance();
5412 $grade = $this->get_user_grade($user->id, false);
5413 $gradingstatus = $this->get_grading_status($user->id);
5415 $gradinginfo = grade_get_grades($this->get_course()->id,
5416 'mod',
5417 'assign',
5418 $instance->id,
5419 $user->id);
5421 $gradingitem = null;
5422 $gradebookgrade = null;
5423 if (isset($gradinginfo->items[0])) {
5424 $gradingitem = $gradinginfo->items[0];
5425 $gradebookgrade = $gradingitem->grades[$user->id];
5428 // Check to see if all feedback plugins are empty.
5429 $emptyplugins = true;
5430 if ($grade) {
5431 foreach ($this->get_feedback_plugins() as $plugin) {
5432 if ($plugin->is_visible() && $plugin->is_enabled()) {
5433 if (!$plugin->is_empty($grade)) {
5434 $emptyplugins = false;
5440 if ($this->get_instance()->markingworkflow && $gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
5441 $emptyplugins = true; // Don't show feedback plugins until released either.
5444 $cangrade = has_capability('mod/assign:grade', $this->get_context());
5445 $hasgrade = $this->get_instance()->grade != GRADE_TYPE_NONE &&
5446 !is_null($gradebookgrade) && !is_null($gradebookgrade->grade);
5447 $gradevisible = $cangrade || $this->get_instance()->grade == GRADE_TYPE_NONE ||
5448 (!is_null($gradebookgrade) && !$gradebookgrade->hidden);
5449 // If there is a visible grade, show the summary.
5450 if (($hasgrade || !$emptyplugins) && $gradevisible) {
5452 $gradefordisplay = null;
5453 $gradeddate = null;
5454 $grader = null;
5455 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5457 $gradingcontrollergrade = '';
5458 if ($hasgrade) {
5459 if ($controller = $gradingmanager->get_active_controller()) {
5460 $menu = make_grades_menu($this->get_instance()->grade);
5461 $controller->set_grade_range($menu, $this->get_instance()->grade > 0);
5462 $gradingcontrollergrade = $controller->render_grade(
5463 $PAGE,
5464 $grade->id,
5465 $gradingitem,
5467 $cangrade
5469 $gradefordisplay = $gradebookgrade->str_long_grade;
5470 } else {
5471 $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
5473 $gradeddate = $gradebookgrade->dategraded;
5475 // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability.
5476 if (has_capability('mod/assign:showhiddengrader', $this->context) || !$this->is_hidden_grader()) {
5477 // Only display the grader if it is in the right state.
5478 if (in_array($gradingstatus, [ASSIGN_GRADING_STATUS_GRADED, ASSIGN_MARKING_WORKFLOW_STATE_RELEASED])) {
5479 if (isset($grade->grader) && $grade->grader > 0) {
5480 $grader = $DB->get_record('user', array('id' => $grade->grader));
5481 } else if (isset($gradebookgrade->usermodified)
5482 && $gradebookgrade->usermodified > 0
5483 && has_capability('mod/assign:grade', $this->get_context(), $gradebookgrade->usermodified)) {
5484 // Grader not provided. Check that usermodified is a user who can grade.
5485 // Case 1: When an assignment is reopened an empty assign_grade is created so the feedback
5486 // plugin can know which attempt it's referring to. In this case, usermodifed is a student.
5487 // Case 2: When an assignment's grade is overrided via the gradebook, usermodified is a grader.
5488 $grader = $DB->get_record('user', array('id' => $gradebookgrade->usermodified));
5494 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5496 if ($grade) {
5497 \mod_assign\event\feedback_viewed::create_from_grade($this, $grade)->trigger();
5499 $feedbackstatus = new assign_feedback_status(
5500 $gradefordisplay,
5501 $gradeddate,
5502 $grader,
5503 $this->get_feedback_plugins(),
5504 $grade,
5505 $this->get_course_module()->id,
5506 $this->get_return_action(),
5507 $this->get_return_params(),
5508 $viewfullnames,
5509 $gradingcontrollergrade
5512 return $feedbackstatus;
5514 return;
5518 * Creates an assign_attempt_history renderable.
5520 * @param stdClass $user the user to get the report for
5521 * @return assign_attempt_history renderable object
5523 public function get_assign_attempt_history_renderable($user) {
5525 $allsubmissions = $this->get_all_submissions($user->id);
5526 $allgrades = $this->get_all_grades($user->id);
5528 $history = new assign_attempt_history($allsubmissions,
5529 $allgrades,
5530 $this->get_submission_plugins(),
5531 $this->get_feedback_plugins(),
5532 $this->get_course_module()->id,
5533 $this->get_return_action(),
5534 $this->get_return_params(),
5535 false,
5538 return $history;
5542 * Print 2 tables of information with no action links -
5543 * the submission summary and the grading summary.
5545 * @param stdClass $user the user to print the report for
5546 * @param bool $showlinks - Return plain text or links to the profile
5547 * @return string - the html summary
5549 public function view_student_summary($user, $showlinks) {
5551 $o = '';
5553 if ($this->can_view_submission($user->id)) {
5554 if (has_capability('mod/assign:viewownsubmissionsummary', $this->get_context(), $user, false)) {
5555 // The user can view the submission summary.
5556 $submissionstatus = $this->get_assign_submission_status_renderable($user, $showlinks);
5557 $o .= $this->get_renderer()->render($submissionstatus);
5560 // If there is a visible grade, show the feedback.
5561 $feedbackstatus = $this->get_assign_feedback_status_renderable($user);
5562 if ($feedbackstatus) {
5563 $o .= $this->get_renderer()->render($feedbackstatus);
5566 // If there is more than one submission, show the history.
5567 $history = $this->get_assign_attempt_history_renderable($user);
5568 if (count($history->submissions) > 1) {
5569 $o .= $this->get_renderer()->render($history);
5572 return $o;
5576 * Returns true if the submit subsission button should be shown to the user.
5578 * @param stdClass $submission The users own submission record.
5579 * @param stdClass $teamsubmission The users team submission record if there is one
5580 * @param int $userid The user
5581 * @return bool
5583 protected function show_submit_button($submission = null, $teamsubmission = null, $userid = null) {
5584 if (!has_capability('mod/assign:submit', $this->get_context(), $userid, false)) {
5585 // The user does not have the capability to submit.
5586 return false;
5588 if ($teamsubmission) {
5589 if ($teamsubmission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5590 // The assignment submission has been completed.
5591 return false;
5592 } else if ($this->submission_empty($teamsubmission)) {
5593 // There is nothing to submit yet.
5594 return false;
5595 } else if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5596 // The user has already clicked the submit button on the team submission.
5597 return false;
5598 } else if (
5599 !empty($this->get_instance()->preventsubmissionnotingroup)
5600 && $this->get_submission_group($userid) == false
5602 return false;
5604 } else if ($submission) {
5605 if ($submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5606 // The assignment submission has been completed.
5607 return false;
5608 } else if ($this->submission_empty($submission)) {
5609 // There is nothing to submit.
5610 return false;
5612 } else {
5613 // We've not got a valid submission or team submission.
5614 return false;
5616 // Last check is that this instance allows drafts.
5617 return $this->get_instance()->submissiondrafts;
5621 * Get the grades for all previous attempts.
5622 * For each grade - the grader is a full user record,
5623 * and gradefordisplay is added (rendered from grading manager).
5625 * @param int $userid If not set, $USER->id will be used.
5626 * @return array $grades All grade records for this user.
5628 protected function get_all_grades($userid) {
5629 global $DB, $USER, $PAGE;
5631 // If the userid is not null then use userid.
5632 if (!$userid) {
5633 $userid = $USER->id;
5636 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
5638 $grades = $DB->get_records('assign_grades', $params, 'attemptnumber ASC');
5640 $gradercache = array();
5641 $cangrade = has_capability('mod/assign:grade', $this->get_context());
5643 // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability.
5644 $showgradername = (
5645 has_capability('mod/assign:showhiddengrader', $this->context, $userid) or
5646 !$this->is_hidden_grader()
5649 // Need gradingitem and gradingmanager.
5650 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5651 $controller = $gradingmanager->get_active_controller();
5653 $gradinginfo = grade_get_grades($this->get_course()->id,
5654 'mod',
5655 'assign',
5656 $this->get_instance()->id,
5657 $userid);
5659 $gradingitem = null;
5660 if (isset($gradinginfo->items[0])) {
5661 $gradingitem = $gradinginfo->items[0];
5664 foreach ($grades as $grade) {
5665 // First lookup the grader info.
5666 if (!$showgradername) {
5667 $grade->grader = null;
5668 } else if (isset($gradercache[$grade->grader])) {
5669 $grade->grader = $gradercache[$grade->grader];
5670 } else if ($grade->grader > 0) {
5671 // Not in cache - need to load the grader record.
5672 $grade->grader = $DB->get_record('user', array('id'=>$grade->grader));
5673 if ($grade->grader) {
5674 $gradercache[$grade->grader->id] = $grade->grader;
5678 // Now get the gradefordisplay.
5679 if ($controller) {
5680 $controller->set_grade_range(make_grades_menu($this->get_instance()->grade), $this->get_instance()->grade > 0);
5681 $grade->gradefordisplay = $controller->render_grade($PAGE,
5682 $grade->id,
5683 $gradingitem,
5684 $grade->grade,
5685 $cangrade);
5686 } else {
5687 $grade->gradefordisplay = $this->display_grade($grade->grade, false);
5692 return $grades;
5696 * Get the submissions for all previous attempts.
5698 * @param int $userid If not set, $USER->id will be used.
5699 * @return array $submissions All submission records for this user (or group).
5701 public function get_all_submissions($userid) {
5702 global $DB, $USER;
5704 // If the userid is not null then use userid.
5705 if (!$userid) {
5706 $userid = $USER->id;
5709 $params = array();
5711 if ($this->get_instance()->teamsubmission) {
5712 $groupid = 0;
5713 $group = $this->get_submission_group($userid);
5714 if ($group) {
5715 $groupid = $group->id;
5718 // Params to get the group submissions.
5719 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
5720 } else {
5721 // Params to get the user submissions.
5722 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
5725 // Return the submissions ordered by attempt.
5726 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber ASC');
5728 return $submissions;
5732 * Creates an assign_grading_summary renderable.
5734 * @param mixed $activitygroup int|null the group for calculating the grading summary (if null the function will determine it)
5735 * @return assign_grading_summary renderable object
5737 public function get_assign_grading_summary_renderable($activitygroup = null) {
5739 $instance = $this->get_default_instance(); // Grading summary requires the raw dates, regardless of relativedates mode.
5740 $cm = $this->get_course_module();
5741 $course = $this->get_course();
5743 $draft = ASSIGN_SUBMISSION_STATUS_DRAFT;
5744 $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5745 $isvisible = $cm->visible;
5747 if ($activitygroup === null) {
5748 $activitygroup = groups_get_activity_group($cm);
5751 if ($instance->teamsubmission) {
5752 $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_NO;
5753 $defaultteammembers = $this->get_submission_group_members(0, true);
5754 if (count($defaultteammembers) > 0) {
5755 if ($instance->preventsubmissionnotingroup) {
5756 $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_REQUIRED;
5757 } else {
5758 $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_OPTIONAL;
5762 $summary = new assign_grading_summary(
5763 $this->count_teams($activitygroup),
5764 $instance->submissiondrafts,
5765 $this->count_submissions_with_status($draft, $activitygroup),
5766 $this->is_any_submission_plugin_enabled(),
5767 $this->count_submissions_with_status($submitted, $activitygroup),
5768 $this->get_cutoffdate($activitygroup),
5769 $this->get_duedate($activitygroup),
5770 $this->get_timelimit($activitygroup),
5771 $this->get_course_module()->id,
5772 $this->count_submissions_need_grading($activitygroup),
5773 $instance->teamsubmission,
5774 $warnofungroupedusers,
5775 $course->relativedatesmode,
5776 $course->startdate,
5777 $this->can_grade(),
5778 $isvisible,
5779 $this->get_course_module()
5781 } else {
5782 // The active group has already been updated in groups_print_activity_menu().
5783 $countparticipants = $this->count_participants($activitygroup);
5784 $summary = new assign_grading_summary(
5785 $countparticipants,
5786 $instance->submissiondrafts,
5787 $this->count_submissions_with_status($draft, $activitygroup),
5788 $this->is_any_submission_plugin_enabled(),
5789 $this->count_submissions_with_status($submitted, $activitygroup),
5790 $this->get_cutoffdate($activitygroup),
5791 $this->get_duedate($activitygroup),
5792 $this->get_timelimit($activitygroup),
5793 $this->get_course_module()->id,
5794 $this->count_submissions_need_grading($activitygroup),
5795 $instance->teamsubmission,
5796 assign_grading_summary::WARN_GROUPS_NO,
5797 $course->relativedatesmode,
5798 $course->startdate,
5799 $this->can_grade(),
5800 $isvisible,
5801 $this->get_course_module()
5805 return $summary;
5809 * Helper function to allow up to fetch the group overrides via one query as opposed to many calls.
5811 * @param int $activitygroup The group we want to check the overrides of
5812 * @return mixed Can return either a fetched DB object, local object or false
5814 private function get_override_data(int $activitygroup) {
5815 global $DB;
5817 $instanceid = $this->get_instance()->id;
5818 $cachekey = "$instanceid-$activitygroup";
5819 if (isset($this->overridedata[$cachekey])) {
5820 return $this->overridedata[$cachekey];
5823 $params = ['groupid' => $activitygroup, 'assignid' => $instanceid];
5824 $this->overridedata[$cachekey] = $DB->get_record('assign_overrides', $params);
5825 return $this->overridedata[$cachekey];
5829 * Return group override duedate.
5831 * @param int $activitygroup Activity active group
5832 * @return int $duedate
5834 private function get_duedate($activitygroup = null) {
5835 if ($activitygroup === null) {
5836 $activitygroup = groups_get_activity_group($this->get_course_module());
5838 if ($this->can_view_grades() && !empty($activitygroup)) {
5839 $groupoverride = $this->get_override_data($activitygroup);
5840 if (!empty($groupoverride->duedate)) {
5841 return $groupoverride->duedate;
5844 return $this->get_instance()->duedate;
5848 * Return group override timelimit.
5850 * @param null|int $activitygroup Activity active group
5851 * @return int $timelimit
5853 private function get_timelimit(?int $activitygroup = null): int {
5854 if ($activitygroup === null) {
5855 $activitygroup = groups_get_activity_group($this->get_course_module());
5857 if ($this->can_view_grades() && !empty($activitygroup)) {
5858 $groupoverride = $this->get_override_data($activitygroup);
5859 if (!empty($groupoverride->timelimit)) {
5860 return $groupoverride->timelimit;
5863 return $this->get_instance()->timelimit;
5867 * Return group override cutoffdate.
5869 * @param null|int $activitygroup Activity active group
5870 * @return int $cutoffdate
5872 private function get_cutoffdate(?int $activitygroup = null): int {
5873 if ($activitygroup === null) {
5874 $activitygroup = groups_get_activity_group($this->get_course_module());
5876 if ($this->can_view_grades() && !empty($activitygroup)) {
5877 $groupoverride = $this->get_override_data($activitygroup);
5878 if (!empty($groupoverride->cutoffdate)) {
5879 return $groupoverride->cutoffdate;
5882 return $this->get_instance()->cutoffdate;
5886 * View submissions page (contains details of current submission).
5888 * @return string
5890 protected function view_submission_page() {
5891 global $CFG, $DB, $USER, $PAGE;
5893 $instance = $this->get_instance();
5895 $this->add_grade_notices();
5897 $o = '';
5899 $postfix = '';
5900 if ($this->has_visible_attachments() && (!$this->get_instance($USER->id)->submissionattachments)) {
5901 $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
5904 $o .= $this->get_renderer()->render(new assign_header($instance,
5905 $this->get_context(),
5906 $this->show_intro(),
5907 $this->get_course_module()->id,
5908 '', '', $postfix));
5910 // Display plugin specific headers.
5911 $plugins = array_merge($this->get_submission_plugins(), $this->get_feedback_plugins());
5912 foreach ($plugins as $plugin) {
5913 if ($plugin->is_enabled() && $plugin->is_visible()) {
5914 $o .= $this->get_renderer()->render(new assign_plugin_header($plugin));
5918 if ($this->can_view_grades()) {
5919 $actionbuttons = new \mod_assign\output\actionmenu($this->get_course_module()->id);
5920 $o .= $this->get_renderer()->submission_actionmenu($actionbuttons);
5922 $summary = $this->get_assign_grading_summary_renderable();
5923 $o .= $this->get_renderer()->render($summary);
5926 if ($this->can_view_submission($USER->id)) {
5927 $o .= $this->view_submission_action_bar($instance, $USER);
5928 $o .= $this->view_student_summary($USER, true);
5931 $o .= $this->view_footer();
5933 \mod_assign\event\submission_status_viewed::create_from_assign($this)->trigger();
5935 return $o;
5939 * The action bar displayed in the submissions page.
5941 * @param stdClass $instance The settings for the current instance of this assignment
5942 * @param stdClass $user The user to print the action bar for
5943 * @return string
5945 public function view_submission_action_bar(stdClass $instance, stdClass $user): string {
5946 $submission = $this->get_user_submission($user->id, false);
5947 // Figure out if we are team or solitary submission.
5948 $teamsubmission = null;
5949 if ($instance->teamsubmission) {
5950 $teamsubmission = $this->get_group_submission($user->id, 0, false);
5953 $showsubmit = ($this->submissions_open($user->id)
5954 && $this->show_submit_button($submission, $teamsubmission, $user->id));
5955 $showedit = ($this->is_any_submission_plugin_enabled()) && $this->can_edit_submission($user->id);
5957 // The method get_group_submission() says that it returns a stdClass, but it can return false >_>.
5958 if ($teamsubmission === false) {
5959 $teamsubmission = new stdClass();
5961 // Same goes for get_user_submission().
5962 if ($submission === false) {
5963 $submission = new stdClass();
5965 $actionbuttons = new \mod_assign\output\user_submission_actionmenu(
5966 $this->get_course_module()->id,
5967 $showsubmit,
5968 $showedit,
5969 $submission,
5970 $teamsubmission,
5971 $instance->timelimit
5974 return $this->get_renderer()->render($actionbuttons);
5978 * Convert the final raw grade(s) in the grading table for the gradebook.
5980 * @param stdClass $grade
5981 * @return array
5983 protected function convert_grade_for_gradebook(stdClass $grade) {
5984 $gradebookgrade = array();
5985 if ($grade->grade >= 0) {
5986 $gradebookgrade['rawgrade'] = $grade->grade;
5988 // Allow "no grade" to be chosen.
5989 if ($grade->grade == -1) {
5990 $gradebookgrade['rawgrade'] = NULL;
5992 $gradebookgrade['userid'] = $grade->userid;
5993 $gradebookgrade['usermodified'] = $grade->grader;
5994 $gradebookgrade['datesubmitted'] = null;
5995 $gradebookgrade['dategraded'] = $grade->timemodified;
5996 if (isset($grade->feedbackformat)) {
5997 $gradebookgrade['feedbackformat'] = $grade->feedbackformat;
5999 if (isset($grade->feedbacktext)) {
6000 $gradebookgrade['feedback'] = $grade->feedbacktext;
6002 if (isset($grade->feedbackfiles)) {
6003 $gradebookgrade['feedbackfiles'] = $grade->feedbackfiles;
6006 return $gradebookgrade;
6010 * Convert submission details for the gradebook.
6012 * @param stdClass $submission
6013 * @return array
6015 protected function convert_submission_for_gradebook(stdClass $submission) {
6016 $gradebookgrade = array();
6018 $gradebookgrade['userid'] = $submission->userid;
6019 $gradebookgrade['usermodified'] = $submission->userid;
6020 $gradebookgrade['datesubmitted'] = $submission->timemodified;
6022 return $gradebookgrade;
6026 * Update grades in the gradebook.
6028 * @param mixed $submission stdClass|null
6029 * @param mixed $grade stdClass|null
6030 * @return bool
6032 protected function gradebook_item_update($submission=null, $grade=null) {
6033 global $CFG;
6035 require_once($CFG->dirroot.'/mod/assign/lib.php');
6036 // Do not push grade to gradebook if blind marking is active as
6037 // the gradebook would reveal the students.
6038 if ($this->is_blind_marking()) {
6039 return false;
6042 // If marking workflow is enabled and grade is not released then remove any grade that may exist in the gradebook.
6043 if ($this->get_instance()->markingworkflow && !empty($grade) &&
6044 $this->get_grading_status($grade->userid) != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
6045 // Remove the grade (if it exists) from the gradebook as it is not 'final'.
6046 $grade->grade = -1;
6047 $grade->feedbacktext = '';
6048 $grade->feebackfiles = [];
6051 if ($submission != null) {
6052 if ($submission->userid == 0) {
6053 // This is a group submission update.
6054 $team = groups_get_members($submission->groupid, 'u.id');
6056 foreach ($team as $member) {
6057 $membersubmission = clone $submission;
6058 $membersubmission->groupid = 0;
6059 $membersubmission->userid = $member->id;
6060 $this->gradebook_item_update($membersubmission, null);
6062 return;
6065 $gradebookgrade = $this->convert_submission_for_gradebook($submission);
6067 } else {
6068 $gradebookgrade = $this->convert_grade_for_gradebook($grade);
6070 // Grading is disabled, return.
6071 if ($this->grading_disabled($gradebookgrade['userid'])) {
6072 return false;
6074 $assign = clone $this->get_instance();
6075 $assign->cmidnumber = $this->get_course_module()->idnumber;
6076 // Set assign gradebook feedback plugin status (enabled and visible).
6077 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
6078 return assign_grade_item_update($assign, $gradebookgrade) == GRADE_UPDATE_OK;
6082 * Update team submission.
6084 * @param stdClass $submission
6085 * @param int $userid
6086 * @param bool $updatetime
6087 * @return bool
6089 protected function update_team_submission(stdClass $submission, $userid, $updatetime) {
6090 global $DB;
6092 if ($updatetime) {
6093 $submission->timemodified = time();
6096 // First update the submission for the current user.
6097 $mysubmission = $this->get_user_submission($userid, true, $submission->attemptnumber);
6098 $mysubmission->status = $submission->status;
6100 $this->update_submission($mysubmission, 0, $updatetime, false);
6102 // Now check the team settings to see if this assignment qualifies as submitted or draft.
6103 $team = $this->get_submission_group_members($submission->groupid, true);
6105 $allsubmitted = true;
6106 $anysubmitted = false;
6107 $result = true;
6108 if (!in_array($submission->status, [ASSIGN_SUBMISSION_STATUS_NEW, ASSIGN_SUBMISSION_STATUS_REOPENED])) {
6109 foreach ($team as $member) {
6110 $membersubmission = $this->get_user_submission($member->id, false, $submission->attemptnumber);
6112 // If no submission found for team member and member is active then everyone has not submitted.
6113 if (!$membersubmission || $membersubmission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED
6114 && ($this->is_active_user($member->id))) {
6115 $allsubmitted = false;
6116 if ($anysubmitted) {
6117 break;
6119 } else {
6120 $anysubmitted = true;
6123 if ($this->get_instance()->requireallteammemberssubmit) {
6124 if ($allsubmitted) {
6125 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6126 } else {
6127 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
6129 $result = $DB->update_record('assign_submission', $submission);
6130 } else {
6131 if ($anysubmitted) {
6132 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6133 } else {
6134 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
6136 $result = $DB->update_record('assign_submission', $submission);
6138 } else {
6139 // Set the group submission to reopened.
6140 foreach ($team as $member) {
6141 $membersubmission = $this->get_user_submission($member->id, true, $submission->attemptnumber);
6142 $membersubmission->status = $submission->status;
6143 $result = $DB->update_record('assign_submission', $membersubmission) && $result;
6145 $result = $DB->update_record('assign_submission', $submission) && $result;
6148 $this->gradebook_item_update($submission);
6149 return $result;
6153 * Update grades in the gradebook based on submission time.
6155 * @param stdClass $submission
6156 * @param int $userid
6157 * @param bool $updatetime
6158 * @param bool $teamsubmission
6159 * @return bool
6161 protected function update_submission(stdClass $submission, $userid, $updatetime, $teamsubmission) {
6162 global $DB;
6164 if ($teamsubmission) {
6165 return $this->update_team_submission($submission, $userid, $updatetime);
6168 if ($updatetime) {
6169 $submission->timemodified = time();
6171 $result= $DB->update_record('assign_submission', $submission);
6172 if ($result) {
6173 $this->gradebook_item_update($submission);
6175 return $result;
6179 * Is this assignment open for submissions?
6181 * Check the due date,
6182 * prevent late submissions,
6183 * has this person already submitted,
6184 * is the assignment locked?
6186 * @param int $userid - Optional userid so we can see if a different user can submit
6187 * @param bool $skipenrolled - Skip enrollment checks (because they have been done already)
6188 * @param stdClass $submission - Pre-fetched submission record (or false to fetch it)
6189 * @param stdClass $flags - Pre-fetched user flags record (or false to fetch it)
6190 * @param stdClass $gradinginfo - Pre-fetched user gradinginfo record (or false to fetch it)
6191 * @return bool
6193 public function submissions_open($userid = 0,
6194 $skipenrolled = false,
6195 $submission = false,
6196 $flags = false,
6197 $gradinginfo = false) {
6198 global $USER;
6200 if (!$userid) {
6201 $userid = $USER->id;
6204 $time = time();
6205 $dateopen = true;
6206 $finaldate = false;
6207 if ($this->get_instance()->cutoffdate) {
6208 $finaldate = $this->get_instance()->cutoffdate;
6211 if ($flags === false) {
6212 $flags = $this->get_user_flags($userid, false);
6214 if ($flags && $flags->locked) {
6215 return false;
6218 // User extensions.
6219 if ($finaldate) {
6220 if ($flags && $flags->extensionduedate) {
6221 // Extension can be before cut off date.
6222 if ($flags->extensionduedate > $finaldate) {
6223 $finaldate = $flags->extensionduedate;
6228 if ($finaldate) {
6229 $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time && $time <= $finaldate);
6230 } else {
6231 $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time);
6234 if (!$dateopen) {
6235 return false;
6238 // Now check if this user has already submitted etc.
6239 if (!$skipenrolled && !is_enrolled($this->get_course_context(), $userid)) {
6240 return false;
6242 // Note you can pass null for submission and it will not be fetched.
6243 if ($submission === false) {
6244 if ($this->get_instance()->teamsubmission) {
6245 $submission = $this->get_group_submission($userid, 0, false);
6246 } else {
6247 $submission = $this->get_user_submission($userid, false);
6250 if ($submission) {
6252 if ($this->get_instance()->submissiondrafts && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6253 // Drafts are tracked and the student has submitted the assignment.
6254 return false;
6258 // See if this user grade is locked in the gradebook.
6259 if ($gradinginfo === false) {
6260 $gradinginfo = grade_get_grades($this->get_course()->id,
6261 'mod',
6262 'assign',
6263 $this->get_instance()->id,
6264 array($userid));
6266 if ($gradinginfo &&
6267 isset($gradinginfo->items[0]->grades[$userid]) &&
6268 $gradinginfo->items[0]->grades[$userid]->locked) {
6269 return false;
6272 return true;
6276 * Render the files in file area.
6278 * @param string $component
6279 * @param string $area
6280 * @param int $submissionid
6281 * @return string
6283 public function render_area_files($component, $area, $submissionid) {
6284 global $USER;
6286 return $this->get_renderer()->assign_files($this->context, $submissionid, $area, $component,
6287 $this->course, $this->coursemodule);
6292 * Capability check to make sure this grader can edit this submission.
6294 * @param int $userid - The user whose submission is to be edited
6295 * @param int $graderid (optional) - The user who will do the editing (default to $USER->id).
6296 * @return bool
6298 public function can_edit_submission($userid, $graderid = 0) {
6299 global $USER;
6301 if (empty($graderid)) {
6302 $graderid = $USER->id;
6305 $instance = $this->get_instance();
6306 if ($userid == $graderid &&
6307 $instance->teamsubmission &&
6308 $instance->preventsubmissionnotingroup &&
6309 $this->get_submission_group($userid) == false) {
6310 return false;
6313 if ($userid == $graderid) {
6314 if ($this->submissions_open($userid) &&
6315 has_capability('mod/assign:submit', $this->context, $graderid)) {
6316 // User can edit their own submission.
6317 return true;
6318 } else {
6319 // We need to return here because editothersubmission should never apply to a users own submission.
6320 return false;
6324 if (!has_capability('mod/assign:editothersubmission', $this->context, $graderid)) {
6325 return false;
6328 $cm = $this->get_course_module();
6329 if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS &&
6330 !has_capability('moodle/site:accessallgroups', $this->context, $graderid)) {
6331 $sharedgroupmembers = $this->get_shared_group_members($cm, $graderid);
6332 return in_array($userid, $sharedgroupmembers);
6334 return true;
6338 * Returns IDs of the users who share group membership with the specified user.
6340 * @param stdClass|cm_info $cm Course-module
6341 * @param int $userid User ID
6342 * @return array An array of ID of users.
6344 public function get_shared_group_members($cm, $userid) {
6345 if (!isset($this->sharedgroupmembers[$userid])) {
6346 $this->sharedgroupmembers[$userid] = array();
6347 if ($members = groups_get_activity_shared_group_members($cm, $userid)) {
6348 $this->sharedgroupmembers[$userid] = array_keys($members);
6352 return $this->sharedgroupmembers[$userid];
6356 * Returns a list of teachers that should be grading given submission.
6358 * @param int $userid The submission to grade
6359 * @return array
6361 protected function get_graders($userid) {
6362 // Potential graders should be active users only.
6363 $potentialgraders = get_enrolled_users($this->context, "mod/assign:grade", null, 'u.*', null, null, null, true);
6365 $graders = array();
6366 if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
6367 if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
6368 foreach ($groups as $group) {
6369 foreach ($potentialgraders as $grader) {
6370 if ($grader->id == $userid) {
6371 // Do not send self.
6372 continue;
6374 if (groups_is_member($group->id, $grader->id)) {
6375 $graders[$grader->id] = $grader;
6379 } else {
6380 // User not in group, try to find graders without group.
6381 foreach ($potentialgraders as $grader) {
6382 if ($grader->id == $userid) {
6383 // Do not send self.
6384 continue;
6386 if (!groups_has_membership($this->get_course_module(), $grader->id)) {
6387 $graders[$grader->id] = $grader;
6391 } else {
6392 foreach ($potentialgraders as $grader) {
6393 if ($grader->id == $userid) {
6394 // Do not send self.
6395 continue;
6397 // Must be enrolled.
6398 if (is_enrolled($this->get_course_context(), $grader->id)) {
6399 $graders[$grader->id] = $grader;
6403 return $graders;
6407 * Returns a list of users that should receive notification about given submission.
6409 * @param int $userid The submission to grade
6410 * @return array
6412 protected function get_notifiable_users($userid) {
6413 // Potential users should be active users only.
6414 $potentialusers = get_enrolled_users($this->context, "mod/assign:receivegradernotifications",
6415 null, 'u.*', null, null, null, true);
6417 $notifiableusers = array();
6418 if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
6419 if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
6420 foreach ($groups as $group) {
6421 foreach ($potentialusers as $potentialuser) {
6422 if ($potentialuser->id == $userid) {
6423 // Do not send self.
6424 continue;
6426 if (groups_is_member($group->id, $potentialuser->id)) {
6427 $notifiableusers[$potentialuser->id] = $potentialuser;
6431 } else {
6432 // User not in group, try to find graders without group.
6433 foreach ($potentialusers as $potentialuser) {
6434 if ($potentialuser->id == $userid) {
6435 // Do not send self.
6436 continue;
6438 if (!groups_has_membership($this->get_course_module(), $potentialuser->id)) {
6439 $notifiableusers[$potentialuser->id] = $potentialuser;
6443 } else {
6444 foreach ($potentialusers as $potentialuser) {
6445 if ($potentialuser->id == $userid) {
6446 // Do not send self.
6447 continue;
6449 // Must be enrolled.
6450 if (is_enrolled($this->get_course_context(), $potentialuser->id)) {
6451 $notifiableusers[$potentialuser->id] = $potentialuser;
6455 return $notifiableusers;
6459 * Format a notification for plain text.
6461 * @param string $messagetype
6462 * @param stdClass $info
6463 * @param stdClass $course
6464 * @param stdClass $context
6465 * @param string $modulename
6466 * @param string $assignmentname
6468 protected static function format_notification_message_text($messagetype,
6469 $info,
6470 $course,
6471 $context,
6472 $modulename,
6473 $assignmentname) {
6474 $formatparams = array('context' => $context->get_course_context());
6475 $posttext = format_string($course->shortname, true, $formatparams) .
6476 ' -> ' .
6477 $modulename .
6478 ' -> ' .
6479 format_string($assignmentname, true, $formatparams) . "\n";
6480 $posttext .= '---------------------------------------------------------------------' . "\n";
6481 $posttext .= get_string($messagetype . 'text', 'assign', $info)."\n";
6482 $posttext .= "\n---------------------------------------------------------------------\n";
6483 return $posttext;
6487 * Format a notification for HTML.
6489 * @param string $messagetype
6490 * @param stdClass $info
6491 * @param stdClass $course
6492 * @param stdClass $context
6493 * @param string $modulename
6494 * @param stdClass $coursemodule
6495 * @param string $assignmentname
6497 protected static function format_notification_message_html($messagetype,
6498 $info,
6499 $course,
6500 $context,
6501 $modulename,
6502 $coursemodule,
6503 $assignmentname) {
6504 global $CFG;
6505 $formatparams = array('context' => $context->get_course_context());
6506 $posthtml = '<p><font face="sans-serif">' .
6507 '<a href="' . $CFG->wwwroot . '/course/view.php?id=' . $course->id . '">' .
6508 format_string($course->shortname, true, $formatparams) .
6509 '</a> ->' .
6510 '<a href="' . $CFG->wwwroot . '/mod/assign/index.php?id=' . $course->id . '">' .
6511 $modulename .
6512 '</a> ->' .
6513 '<a href="' . $CFG->wwwroot . '/mod/assign/view.php?id=' . $coursemodule->id . '">' .
6514 format_string($assignmentname, true, $formatparams) .
6515 '</a></font></p>';
6516 $posthtml .= '<hr /><font face="sans-serif">';
6517 $posthtml .= '<p>' . get_string($messagetype . 'html', 'assign', $info) . '</p>';
6518 $posthtml .= '</font><hr />';
6519 return $posthtml;
6523 * Message someone about something (static so it can be called from cron).
6525 * @param stdClass $userfrom
6526 * @param stdClass $userto
6527 * @param string $messagetype
6528 * @param string $eventtype
6529 * @param int $updatetime
6530 * @param stdClass $coursemodule
6531 * @param stdClass $context
6532 * @param stdClass $course
6533 * @param string $modulename
6534 * @param string $assignmentname
6535 * @param bool $blindmarking
6536 * @param int $uniqueidforuser
6537 * @return void
6539 public static function send_assignment_notification($userfrom,
6540 $userto,
6541 $messagetype,
6542 $eventtype,
6543 $updatetime,
6544 $coursemodule,
6545 $context,
6546 $course,
6547 $modulename,
6548 $assignmentname,
6549 $blindmarking,
6550 $uniqueidforuser) {
6551 global $CFG, $PAGE;
6553 $info = new stdClass();
6554 if ($blindmarking) {
6555 $userfrom = clone($userfrom);
6556 $info->username = get_string('participant', 'assign') . ' ' . $uniqueidforuser;
6557 $userfrom->firstname = get_string('participant', 'assign');
6558 $userfrom->lastname = $uniqueidforuser;
6559 $userfrom->email = $CFG->noreplyaddress;
6560 } else {
6561 $info->username = fullname($userfrom, true);
6563 $info->assignment = format_string($assignmentname, true, array('context'=>$context));
6564 $info->url = $CFG->wwwroot.'/mod/assign/view.php?id='.$coursemodule->id;
6565 $info->timeupdated = userdate($updatetime, get_string('strftimerecentfull'));
6567 $postsubject = get_string($messagetype . 'small', 'assign', $info);
6568 $posttext = self::format_notification_message_text($messagetype,
6569 $info,
6570 $course,
6571 $context,
6572 $modulename,
6573 $assignmentname);
6574 $posthtml = '';
6575 if ($userto->mailformat == 1) {
6576 $posthtml = self::format_notification_message_html($messagetype,
6577 $info,
6578 $course,
6579 $context,
6580 $modulename,
6581 $coursemodule,
6582 $assignmentname);
6585 $eventdata = new \core\message\message();
6586 $eventdata->courseid = $course->id;
6587 $eventdata->modulename = 'assign';
6588 $eventdata->userfrom = $userfrom;
6589 $eventdata->userto = $userto;
6590 $eventdata->subject = $postsubject;
6591 $eventdata->fullmessage = $posttext;
6592 $eventdata->fullmessageformat = FORMAT_PLAIN;
6593 $eventdata->fullmessagehtml = $posthtml;
6594 $eventdata->smallmessage = $postsubject;
6596 $eventdata->name = $eventtype;
6597 $eventdata->component = 'mod_assign';
6598 $eventdata->notification = 1;
6599 $eventdata->contexturl = $info->url;
6600 $eventdata->contexturlname = $info->assignment;
6601 $customdata = [
6602 'cmid' => $coursemodule->id,
6603 'instance' => $coursemodule->instance,
6604 'messagetype' => $messagetype,
6605 'blindmarking' => $blindmarking,
6606 'uniqueidforuser' => $uniqueidforuser,
6608 // Check if the userfrom is real and visible.
6609 if (!empty($userfrom->id) && core_user::is_real_user($userfrom->id)) {
6610 $userpicture = new user_picture($userfrom);
6611 $userpicture->size = 1; // Use f1 size.
6612 $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
6613 $customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false);
6615 $eventdata->customdata = $customdata;
6617 message_send($eventdata);
6621 * Message someone about something.
6623 * @param stdClass $userfrom
6624 * @param stdClass $userto
6625 * @param string $messagetype
6626 * @param string $eventtype
6627 * @param int $updatetime
6628 * @return void
6630 public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime) {
6631 global $USER;
6632 $userid = core_user::is_real_user($userfrom->id) ? $userfrom->id : $USER->id;
6633 $uniqueid = $this->get_uniqueid_for_user($userid);
6634 self::send_assignment_notification($userfrom,
6635 $userto,
6636 $messagetype,
6637 $eventtype,
6638 $updatetime,
6639 $this->get_course_module(),
6640 $this->get_context(),
6641 $this->get_course(),
6642 $this->get_module_name(),
6643 $this->get_instance()->name,
6644 $this->is_blind_marking(),
6645 $uniqueid);
6649 * Notify student upon successful submission copy.
6651 * @param stdClass $submission
6652 * @return void
6654 protected function notify_student_submission_copied(stdClass $submission) {
6655 global $DB, $USER;
6657 $adminconfig = $this->get_admin_config();
6658 // Use the same setting for this - no need for another one.
6659 if (empty($adminconfig->submissionreceipts)) {
6660 // No need to do anything.
6661 return;
6663 if ($submission->userid) {
6664 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6665 } else {
6666 $user = $USER;
6668 $this->send_notification($user,
6669 $user,
6670 'submissioncopied',
6671 'assign_notification',
6672 $submission->timemodified);
6675 * Notify student upon successful submission.
6677 * @param stdClass $submission
6678 * @return void
6680 protected function notify_student_submission_receipt(stdClass $submission) {
6681 global $DB, $USER;
6683 $adminconfig = $this->get_admin_config();
6684 if (empty($adminconfig->submissionreceipts)) {
6685 // No need to do anything.
6686 return;
6688 if ($submission->userid) {
6689 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6690 } else {
6691 $user = $USER;
6693 if ($submission->userid == $USER->id) {
6694 $this->send_notification(core_user::get_noreply_user(),
6695 $user,
6696 'submissionreceipt',
6697 'assign_notification',
6698 $submission->timemodified);
6699 } else {
6700 $this->send_notification($USER,
6701 $user,
6702 'submissionreceiptother',
6703 'assign_notification',
6704 $submission->timemodified);
6709 * Send notifications to graders upon student submissions.
6711 * @param stdClass $submission
6712 * @return void
6714 protected function notify_graders(stdClass $submission) {
6715 global $DB, $USER;
6717 $instance = $this->get_instance();
6719 $late = $instance->duedate && ($instance->duedate < time());
6721 if (!$instance->sendnotifications && !($late && $instance->sendlatenotifications)) {
6722 // No need to do anything.
6723 return;
6726 if ($submission->userid) {
6727 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6728 } else {
6729 $user = $USER;
6732 if ($notifyusers = $this->get_notifiable_users($user->id)) {
6733 foreach ($notifyusers as $notifyuser) {
6734 $this->send_notification($user,
6735 $notifyuser,
6736 'gradersubmissionupdated',
6737 'assign_notification',
6738 $submission->timemodified);
6744 * Submit a submission for grading.
6746 * @param stdClass $data - The form data
6747 * @param array $notices - List of error messages to display on an error condition.
6748 * @return bool Return false if the submission was not submitted.
6750 public function submit_for_grading($data, $notices) {
6751 global $USER;
6753 $userid = $USER->id;
6754 if (!empty($data->userid)) {
6755 $userid = $data->userid;
6757 // Need submit permission to submit an assignment.
6758 if ($userid == $USER->id) {
6759 require_capability('mod/assign:submit', $this->context);
6760 } else {
6761 if (!$this->can_edit_submission($userid, $USER->id)) {
6762 throw new \moodle_exception('nopermission');
6766 $instance = $this->get_instance();
6768 if ($instance->teamsubmission) {
6769 $submission = $this->get_group_submission($userid, 0, true);
6770 } else {
6771 $submission = $this->get_user_submission($userid, true);
6774 if (!$this->submissions_open($userid)) {
6775 $notices[] = get_string('submissionsclosed', 'assign');
6776 return false;
6779 $adminconfig = $this->get_admin_config();
6781 $submissionstatement = '';
6782 if ($instance->requiresubmissionstatement) {
6783 $submissionstatement = $this->get_submissionstatement($adminconfig, $instance, $this->context);
6786 if (!empty($submissionstatement) && $instance->requiresubmissionstatement
6787 && empty($data->submissionstatement) && $USER->id == $userid) {
6788 return false;
6791 if ($submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6792 // Give each submission plugin a chance to process the submission.
6793 $plugins = $this->get_submission_plugins();
6794 foreach ($plugins as $plugin) {
6795 if ($plugin->is_enabled() && $plugin->is_visible()) {
6796 $plugin->submit_for_grading($submission);
6800 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6801 $this->update_submission($submission, $userid, true, $instance->teamsubmission);
6802 $completion = new completion_info($this->get_course());
6803 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
6804 $this->update_activity_completion_records($instance->teamsubmission,
6805 $instance->requireallteammemberssubmit,
6806 $submission,
6807 $userid,
6808 COMPLETION_COMPLETE,
6809 $completion);
6812 if (!empty($data->submissionstatement) && $USER->id == $userid) {
6813 \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
6815 $this->notify_graders($submission);
6816 $this->notify_student_submission_receipt($submission);
6818 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, false)->trigger();
6820 return true;
6822 $notices[] = get_string('submissionsclosed', 'assign');
6823 return false;
6827 * A students submission is submitted for grading by a teacher.
6829 * @return bool
6831 protected function process_submit_other_for_grading($mform, $notices) {
6832 global $USER, $CFG;
6834 require_sesskey();
6836 $userid = optional_param('userid', $USER->id, PARAM_INT);
6838 if (!$this->submissions_open($userid)) {
6839 $notices[] = get_string('submissionsclosed', 'assign');
6840 return false;
6842 $data = new stdClass();
6843 $data->userid = $userid;
6844 return $this->submit_for_grading($data, $notices);
6848 * Assignment submission is processed before grading.
6850 * @param moodleform|null $mform If validation failed when submitting this form - this is the moodleform.
6851 * It can be null.
6852 * @return bool Return false if the validation fails. This affects which page is displayed next.
6854 protected function process_submit_for_grading($mform, $notices) {
6855 global $CFG;
6857 require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
6858 require_sesskey();
6860 if (!$this->submissions_open()) {
6861 $notices[] = get_string('submissionsclosed', 'assign');
6862 return false;
6865 $data = new stdClass();
6866 $adminconfig = $this->get_admin_config();
6867 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
6869 $submissionstatement = '';
6870 if ($requiresubmissionstatement) {
6871 $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
6874 // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
6875 // that the submission statement checkbox will be displayed.
6876 if (empty($submissionstatement)) {
6877 $requiresubmissionstatement = false;
6880 if ($mform == null) {
6881 $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
6882 $submissionstatement,
6883 $this->get_course_module()->id,
6884 $data));
6887 $data = $mform->get_data();
6888 if (!$mform->is_cancelled()) {
6889 if ($mform->get_data() == false) {
6890 return false;
6892 return $this->submit_for_grading($data, $notices);
6894 return true;
6898 * Save the extension date for a single user.
6900 * @param int $userid The user id
6901 * @param mixed $extensionduedate Either an integer date or null
6902 * @return boolean
6904 public function save_user_extension($userid, $extensionduedate) {
6905 global $DB, $CFG;
6906 require_once($CFG->dirroot.'/calendar/lib.php');
6908 // Need submit permission to submit an assignment.
6909 require_capability('mod/assign:grantextension', $this->context);
6911 if (!is_enrolled($this->get_course_context(), $userid)) {
6912 return false;
6914 if (!has_capability('mod/assign:submit', $this->context, $userid)) {
6915 return false;
6918 if ($this->get_instance()->duedate && $extensionduedate) {
6919 if ($this->get_instance()->duedate > $extensionduedate) {
6920 return false;
6923 if ($this->get_instance()->allowsubmissionsfromdate && $extensionduedate) {
6924 if ($this->get_instance()->allowsubmissionsfromdate > $extensionduedate) {
6925 return false;
6929 $flags = $this->get_user_flags($userid, true);
6930 $flags->extensionduedate = $extensionduedate;
6932 $result = $this->update_user_flags($flags);
6934 if ($result) {
6935 \mod_assign\event\extension_granted::create_from_assign($this, $userid)->trigger();
6937 $cm = $this->get_course_module();
6938 $instance = $this->get_instance();
6940 if ($extensionduedate) {
6941 $event = $DB->get_record('event', [
6942 'userid' => $userid,
6943 'eventtype' => ASSIGN_EVENT_TYPE_EXTENSION,
6944 'modulename' => 'assign',
6945 'instance' => $instance->id,
6948 if ($event) {
6949 $event->timestart = $extensionduedate;
6950 $DB->update_record('event', $event);
6951 } else {
6952 $event = new stdClass();
6953 $event->type = CALENDAR_EVENT_TYPE_ACTION;
6954 $event->name = get_string('calendarextension', 'assign', $instance->name);
6955 $event->description = format_module_intro('assign', $instance, $cm->id);
6956 $event->format = FORMAT_HTML;
6957 $event->courseid = 0;
6958 $event->groupid = 0;
6959 $event->userid = $userid;
6960 $event->modulename = 'assign';
6961 $event->instance = $instance->id;
6962 $event->timestart = $extensionduedate;
6963 $event->timeduration = 0;
6964 $event->visible = instance_is_visible('assign', $instance);
6965 $event->eventtype = ASSIGN_EVENT_TYPE_EXTENSION;
6966 $event->priority = null;
6968 calendar_event::create($event, false);
6970 } else {
6971 $DB->delete_records('event', [
6972 'userid' => $userid,
6973 'eventtype' => ASSIGN_EVENT_TYPE_EXTENSION,
6974 'modulename' => 'assign',
6975 'instance' => $instance->id,
6979 return $result;
6983 * Save extension date.
6985 * @param moodleform $mform The submitted form
6986 * @return boolean
6988 protected function process_save_extension(& $mform) {
6989 global $DB, $CFG;
6991 // Include extension form.
6992 require_once($CFG->dirroot . '/mod/assign/extensionform.php');
6993 require_sesskey();
6995 $users = optional_param('userid', 0, PARAM_INT);
6996 if (!$users) {
6997 $users = required_param('selectedusers', PARAM_SEQUENCE);
6999 $userlist = explode(',', $users);
7001 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
7002 $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
7003 foreach ($userlist as $userid) {
7004 // To validate extension date with users overrides.
7005 $override = $this->override_exists($userid);
7006 foreach ($keys as $key) {
7007 if ($override->{$key}) {
7008 if ($maxoverride[$key] < $override->{$key}) {
7009 $maxoverride[$key] = $override->{$key};
7011 } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
7012 $maxoverride[$key] = $this->get_instance()->{$key};
7016 foreach ($keys as $key) {
7017 if ($maxoverride[$key]) {
7018 $this->get_instance()->{$key} = $maxoverride[$key];
7022 $formparams = array(
7023 'instance' => $this->get_instance(),
7024 'assign' => $this,
7025 'userlist' => $userlist
7028 $mform = new mod_assign_extension_form(null, $formparams);
7030 if ($mform->is_cancelled()) {
7031 return true;
7034 if ($formdata = $mform->get_data()) {
7035 if (!empty($formdata->selectedusers)) {
7036 $users = explode(',', $formdata->selectedusers);
7037 $result = true;
7038 foreach ($users as $userid) {
7039 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7040 $result = $this->save_user_extension($user->id, $formdata->extensionduedate) && $result;
7042 return $result;
7044 if (!empty($formdata->userid)) {
7045 $user = $DB->get_record('user', array('id' => $formdata->userid), '*', MUST_EXIST);
7046 return $this->save_user_extension($user->id, $formdata->extensionduedate);
7050 return false;
7054 * Save quick grades.
7056 * @return string The result of the save operation
7058 protected function process_save_quick_grades() {
7059 global $USER, $DB, $CFG;
7061 // Need grade permission.
7062 require_capability('mod/assign:grade', $this->context);
7063 require_sesskey();
7065 // Make sure advanced grading is disabled.
7066 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
7067 $controller = $gradingmanager->get_active_controller();
7068 if (!empty($controller)) {
7069 $message = get_string('errorquickgradingvsadvancedgrading', 'assign');
7070 $this->set_error_message($message);
7071 return $message;
7074 $users = array();
7075 // First check all the last modified values.
7076 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
7077 $participants = $this->list_participants($currentgroup, true);
7079 // Gets a list of possible users and look for values based upon that.
7080 foreach ($participants as $userid => $unused) {
7081 $modified = optional_param('grademodified_' . $userid, -1, PARAM_INT);
7082 $attemptnumber = optional_param('gradeattempt_' . $userid, -1, PARAM_INT);
7083 // Gather the userid, updated grade and last modified value.
7084 $record = new stdClass();
7085 $record->userid = $userid;
7086 if ($modified >= 0) {
7087 $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT));
7088 $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_ALPHA);
7089 $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT);
7090 } else {
7091 // This user was not in the grading table.
7092 continue;
7094 $record->attemptnumber = $attemptnumber;
7095 $record->lastmodified = $modified;
7096 $record->gradinginfo = grade_get_grades($this->get_course()->id,
7097 'mod',
7098 'assign',
7099 $this->get_instance()->id,
7100 array($userid));
7101 $users[$userid] = $record;
7104 if (empty($users)) {
7105 $message = get_string('nousersselected', 'assign');
7106 $this->set_error_message($message);
7107 return $message;
7110 list($userids, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED);
7111 $params['assignid1'] = $this->get_instance()->id;
7112 $params['assignid2'] = $this->get_instance()->id;
7114 // Check them all for currency.
7115 $grademaxattempt = 'SELECT s.userid, s.attemptnumber AS maxattempt
7116 FROM {assign_submission} s
7117 WHERE s.assignment = :assignid1 AND s.latest = 1';
7119 $sql = 'SELECT u.id AS userid, g.grade AS grade, g.timemodified AS lastmodified,
7120 uf.workflowstate, uf.allocatedmarker, gmx.maxattempt AS attemptnumber
7121 FROM {user} u
7122 LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
7123 LEFT JOIN {assign_grades} g ON
7124 u.id = g.userid AND
7125 g.assignment = :assignid2 AND
7126 g.attemptnumber = gmx.maxattempt
7127 LEFT JOIN {assign_user_flags} uf ON uf.assignment = g.assignment AND uf.userid = g.userid
7128 WHERE u.id ' . $userids;
7129 $currentgrades = $DB->get_recordset_sql($sql, $params);
7131 $modifiedusers = array();
7132 foreach ($currentgrades as $current) {
7133 $modified = $users[(int)$current->userid];
7134 $grade = $this->get_user_grade($modified->userid, false);
7135 // Check to see if the grade column was even visible.
7136 $gradecolpresent = optional_param('quickgrade_' . $modified->userid, false, PARAM_INT) !== false;
7138 // Check to see if the outcomes were modified.
7139 if ($CFG->enableoutcomes) {
7140 foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
7141 $oldoutcome = $outcome->grades[$modified->userid]->grade;
7142 $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
7143 $newoutcome = optional_param($paramname, -1, PARAM_FLOAT);
7144 // Check to see if the outcome column was even visible.
7145 $outcomecolpresent = optional_param($paramname, false, PARAM_FLOAT) !== false;
7146 if ($outcomecolpresent && ($oldoutcome != $newoutcome)) {
7147 // Can't check modified time for outcomes because it is not reported.
7148 $modifiedusers[$modified->userid] = $modified;
7149 continue;
7154 // Let plugins participate.
7155 foreach ($this->feedbackplugins as $plugin) {
7156 if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
7157 // The plugins must handle is_quickgrading_modified correctly - ie
7158 // handle hidden columns.
7159 if ($plugin->is_quickgrading_modified($modified->userid, $grade)) {
7160 if ((int)$current->lastmodified > (int)$modified->lastmodified) {
7161 $message = get_string('errorrecordmodified', 'assign');
7162 $this->set_error_message($message);
7163 return $message;
7164 } else {
7165 $modifiedusers[$modified->userid] = $modified;
7166 continue;
7172 if (($current->grade < 0 || $current->grade === null) &&
7173 ($modified->grade < 0 || $modified->grade === null)) {
7174 // Different ways to indicate no grade.
7175 $modified->grade = $current->grade; // Keep existing grade.
7177 // Treat 0 and null as different values.
7178 if ($current->grade !== null) {
7179 $current->grade = floatval($current->grade);
7181 $gradechanged = $gradecolpresent && grade_floats_different($current->grade, $modified->grade);
7182 $markingallocationchanged = $this->get_instance()->markingworkflow &&
7183 $this->get_instance()->markingallocation &&
7184 ($modified->allocatedmarker !== false) &&
7185 ($current->allocatedmarker != $modified->allocatedmarker);
7186 $workflowstatechanged = $this->get_instance()->markingworkflow &&
7187 ($modified->workflowstate !== false) &&
7188 ($current->workflowstate != $modified->workflowstate);
7189 if ($gradechanged || $markingallocationchanged || $workflowstatechanged) {
7190 // Grade changed.
7191 if ($this->grading_disabled($modified->userid)) {
7192 continue;
7194 $badmodified = (int)$current->lastmodified > (int)$modified->lastmodified;
7195 $badattempt = (int)$current->attemptnumber != (int)$modified->attemptnumber;
7196 if ($badmodified || $badattempt) {
7197 // Error - record has been modified since viewing the page.
7198 $message = get_string('errorrecordmodified', 'assign');
7199 $this->set_error_message($message);
7200 return $message;
7201 } else {
7202 $modifiedusers[$modified->userid] = $modified;
7207 $currentgrades->close();
7209 $adminconfig = $this->get_admin_config();
7210 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7212 // Ok - ready to process the updates.
7213 foreach ($modifiedusers as $userid => $modified) {
7214 $grade = $this->get_user_grade($userid, true);
7215 $flags = $this->get_user_flags($userid, true);
7216 $grade->grade= grade_floatval(unformat_float($modified->grade));
7217 $grade->grader= $USER->id;
7218 $gradecolpresent = optional_param('quickgrade_' . $userid, false, PARAM_INT) !== false;
7220 // Save plugins data.
7221 foreach ($this->feedbackplugins as $plugin) {
7222 if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
7223 $plugin->save_quickgrading_changes($userid, $grade);
7224 if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
7225 // This is the feedback plugin chose to push comments to the gradebook.
7226 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
7227 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
7228 $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
7233 // These will be set to false if they are not present in the quickgrading
7234 // form (e.g. column hidden).
7235 $workflowstatemodified = ($modified->workflowstate !== false) &&
7236 ($flags->workflowstate != $modified->workflowstate);
7238 $allocatedmarkermodified = ($modified->allocatedmarker !== false) &&
7239 ($flags->allocatedmarker != $modified->allocatedmarker);
7241 if ($workflowstatemodified) {
7242 $flags->workflowstate = $modified->workflowstate;
7244 if ($allocatedmarkermodified) {
7245 $flags->allocatedmarker = $modified->allocatedmarker;
7247 if ($workflowstatemodified || $allocatedmarkermodified) {
7248 if ($this->update_user_flags($flags) && $workflowstatemodified) {
7249 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7250 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $flags->workflowstate)->trigger();
7253 $this->update_grade($grade);
7255 // Allow teachers to skip sending notifications.
7256 if (optional_param('sendstudentnotifications', true, PARAM_BOOL)) {
7257 $this->notify_grade_modified($grade, true);
7260 // Save outcomes.
7261 if ($CFG->enableoutcomes) {
7262 $data = array();
7263 foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
7264 $oldoutcome = $outcome->grades[$modified->userid]->grade;
7265 $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
7266 // This will be false if the input was not in the quickgrading
7267 // form (e.g. column hidden).
7268 $newoutcome = optional_param($paramname, false, PARAM_INT);
7269 if ($newoutcome !== false && ($oldoutcome != $newoutcome)) {
7270 $data[$outcomeid] = $newoutcome;
7273 if (count($data) > 0) {
7274 grade_update_outcomes('mod/assign',
7275 $this->course->id,
7276 'mod',
7277 'assign',
7278 $this->get_instance()->id,
7279 $userid,
7280 $data);
7285 return get_string('quickgradingchangessaved', 'assign');
7289 * Reveal student identities to markers (and the gradebook).
7291 * @return void
7293 public function reveal_identities() {
7294 global $DB;
7296 require_capability('mod/assign:revealidentities', $this->context);
7298 if ($this->get_instance()->revealidentities || empty($this->get_instance()->blindmarking)) {
7299 return false;
7302 // Update the assignment record.
7303 $update = new stdClass();
7304 $update->id = $this->get_instance()->id;
7305 $update->revealidentities = 1;
7306 $DB->update_record('assign', $update);
7308 // Refresh the instance data.
7309 $this->instance = null;
7311 // Release the grades to the gradebook.
7312 // First create the column in the gradebook.
7313 $this->update_gradebook(false, $this->get_course_module()->id);
7315 // Now release all grades.
7317 $adminconfig = $this->get_admin_config();
7318 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7319 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
7320 $grades = $DB->get_records('assign_grades', array('assignment'=>$this->get_instance()->id));
7322 $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
7324 foreach ($grades as $grade) {
7325 // Fetch any comments for this student.
7326 if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
7327 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
7328 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
7329 $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
7331 $this->gradebook_item_update(null, $grade);
7334 \mod_assign\event\identities_revealed::create_from_assign($this)->trigger();
7338 * Reveal student identities to markers (and the gradebook).
7340 * @return void
7342 protected function process_reveal_identities() {
7344 if (!confirm_sesskey()) {
7345 return false;
7348 return $this->reveal_identities();
7353 * Save grading options.
7355 * @deprecated since Moodle 4.5
7356 * @todo Final deprecation in Moodle 6.0. See MDL-82876.
7357 * @return void
7359 #[\core\attribute\deprecated(
7360 'null',
7361 since: '4.5',
7362 reason: 'It is no longer used.',
7363 mdl: 'MDL-82681',
7365 protected function process_save_grading_options() {
7366 \core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
7370 * @deprecated since 2.7
7372 public function format_grade_for_log() {
7373 throw new coding_exception(__FUNCTION__ . ' has been deprecated, please do not use it any more');
7377 * @deprecated since 2.7
7379 public function format_submission_for_log() {
7380 throw new coding_exception(__FUNCTION__ . ' has been deprecated, please do not use it any more');
7384 * Require a valid sess key and then call copy_previous_attempt.
7386 * @param array $notices Any error messages that should be shown
7387 * to the user at the top of the edit submission form.
7388 * @return bool
7390 protected function process_copy_previous_attempt(&$notices) {
7391 require_sesskey();
7393 return $this->copy_previous_attempt($notices);
7397 * Copy the current assignment submission from the last submitted attempt.
7399 * @param array $notices Any error messages that should be shown
7400 * to the user at the top of the edit submission form.
7401 * @return bool
7403 public function copy_previous_attempt(&$notices) {
7404 global $USER, $CFG;
7406 require_capability('mod/assign:submit', $this->context);
7408 $instance = $this->get_instance();
7409 if ($instance->teamsubmission) {
7410 $submission = $this->get_group_submission($USER->id, 0, true);
7411 } else {
7412 $submission = $this->get_user_submission($USER->id, true);
7414 if (!$submission || $submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
7415 $notices[] = get_string('submissionnotcopiedinvalidstatus', 'assign');
7416 return false;
7418 $flags = $this->get_user_flags($USER->id, false);
7420 // Get the flags to check if it is locked.
7421 if ($flags && $flags->locked) {
7422 $notices[] = get_string('submissionslocked', 'assign');
7423 return false;
7425 if ($instance->submissiondrafts) {
7426 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
7427 } else {
7428 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
7430 $this->update_submission($submission, $USER->id, true, $instance->teamsubmission);
7432 // Find the previous submission.
7433 if ($instance->teamsubmission) {
7434 $previoussubmission = $this->get_group_submission($USER->id, 0, true, $submission->attemptnumber - 1);
7435 } else {
7436 $previoussubmission = $this->get_user_submission($USER->id, true, $submission->attemptnumber - 1);
7439 if (!$previoussubmission) {
7440 // There was no previous submission so there is nothing else to do.
7441 return true;
7444 $pluginerror = false;
7445 foreach ($this->get_submission_plugins() as $plugin) {
7446 if ($plugin->is_visible() && $plugin->is_enabled()) {
7447 if (!$plugin->copy_submission($previoussubmission, $submission)) {
7448 $notices[] = $plugin->get_error();
7449 $pluginerror = true;
7453 if ($pluginerror) {
7454 return false;
7457 \mod_assign\event\submission_duplicated::create_from_submission($this, $submission)->trigger();
7459 $complete = COMPLETION_INCOMPLETE;
7460 if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
7461 $complete = COMPLETION_COMPLETE;
7463 $completion = new completion_info($this->get_course());
7464 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
7465 $this->update_activity_completion_records($instance->teamsubmission,
7466 $instance->requireallteammemberssubmit,
7467 $submission,
7468 $USER->id,
7469 $complete,
7470 $completion);
7473 if (!$instance->submissiondrafts) {
7474 // There is a case for not notifying the student about the submission copy,
7475 // but it provides a record of the event and if they then cancel editing it
7476 // is clear that the submission was copied.
7477 $this->notify_student_submission_copied($submission);
7478 $this->notify_graders($submission);
7480 // The same logic applies here - we could not notify teachers,
7481 // but then they would wonder why there are submitted assignments
7482 // and they haven't been notified.
7483 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
7485 return true;
7489 * Determine if the current submission is empty or not.
7491 * @param submission $submission the students submission record to check.
7492 * @return bool
7494 public function submission_empty($submission) {
7495 $allempty = true;
7497 foreach ($this->submissionplugins as $plugin) {
7498 if ($plugin->is_enabled() && $plugin->is_visible()) {
7499 if (!$allempty || !$plugin->is_empty($submission)) {
7500 $allempty = false;
7504 return $allempty;
7508 * Determine if a new submission is empty or not
7510 * @param stdClass $data Submission data
7511 * @return bool
7513 public function new_submission_empty($data) {
7514 foreach ($this->submissionplugins as $plugin) {
7515 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions() &&
7516 !$plugin->submission_is_empty($data)) {
7517 return false;
7520 return true;
7524 * Save assignment submission for the current user.
7526 * @param stdClass $data
7527 * @param array $notices Any error messages that should be shown
7528 * to the user.
7529 * @return bool
7531 public function save_submission(stdClass $data, & $notices) {
7532 global $CFG, $USER, $DB;
7534 $userid = $USER->id;
7535 if (!empty($data->userid)) {
7536 $userid = $data->userid;
7539 $user = clone($USER);
7540 if ($userid == $USER->id) {
7541 require_capability('mod/assign:submit', $this->context);
7542 } else {
7543 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
7544 if (!$this->can_edit_submission($userid, $USER->id)) {
7545 throw new \moodle_exception('nopermission');
7548 $instance = $this->get_instance();
7550 if ($instance->teamsubmission) {
7551 $submission = $this->get_group_submission($userid, 0, true);
7552 } else {
7553 $submission = $this->get_user_submission($userid, true);
7556 if ($this->new_submission_empty($data)) {
7557 $notices[] = get_string('submissionempty', 'mod_assign');
7558 return false;
7561 // Check that no one has modified the submission since we started looking at it.
7562 if (isset($data->lastmodified) && ($submission->timemodified > $data->lastmodified)) {
7563 // Another user has submitted something. Notify the current user.
7564 if ($submission->status !== ASSIGN_SUBMISSION_STATUS_NEW) {
7565 $notices[] = $instance->teamsubmission ? get_string('submissionmodifiedgroup', 'mod_assign')
7566 : get_string('submissionmodified', 'mod_assign');
7567 return false;
7571 if ($instance->submissiondrafts) {
7572 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
7573 } else {
7574 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
7577 $flags = $this->get_user_flags($userid, false);
7579 // Get the flags to check if it is locked.
7580 if ($flags && $flags->locked) {
7581 throw new \moodle_exception('submissionslocked', 'assign');
7582 return true;
7585 $pluginerror = false;
7586 foreach ($this->submissionplugins as $plugin) {
7587 if ($plugin->is_enabled() && $plugin->is_visible()) {
7588 if (!$plugin->save($submission, $data)) {
7589 $notices[] = $plugin->get_error();
7590 $pluginerror = true;
7595 $allempty = $this->submission_empty($submission);
7596 if ($pluginerror || $allempty) {
7597 if ($allempty) {
7598 $notices[] = get_string('submissionempty', 'mod_assign');
7600 return false;
7603 $this->update_submission($submission, $userid, true, $instance->teamsubmission);
7604 $users = [$userid];
7606 if ($instance->teamsubmission && !$instance->requireallteammemberssubmit) {
7607 $team = $this->get_submission_group_members($submission->groupid, true);
7609 foreach ($team as $member) {
7610 if ($member->id != $userid) {
7611 $membersubmission = clone($submission);
7612 $this->update_submission($membersubmission, $member->id, true, $instance->teamsubmission);
7613 $users[] = $member->id;
7618 $complete = COMPLETION_INCOMPLETE;
7619 if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
7620 $complete = COMPLETION_COMPLETE;
7623 $completion = new completion_info($this->get_course());
7624 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
7625 foreach ($users as $id) {
7626 $completion->update_state($this->get_course_module(), $complete, $id);
7630 // Logging.
7631 if (isset($data->submissionstatement) && ($userid == $USER->id)) {
7632 \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
7635 if (!$instance->submissiondrafts) {
7636 $this->notify_student_submission_receipt($submission);
7637 $this->notify_graders($submission);
7638 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
7640 return true;
7644 * Save assignment submission.
7646 * @param moodleform $mform
7647 * @param array $notices Any error messages that should be shown
7648 * to the user at the top of the edit submission form.
7649 * @return bool
7651 protected function process_save_submission(&$mform, &$notices) {
7652 global $CFG, $USER;
7654 // Include submission form.
7655 require_once($CFG->dirroot . '/mod/assign/submission_form.php');
7657 $userid = optional_param('userid', $USER->id, PARAM_INT);
7658 // Need submit permission to submit an assignment.
7659 require_sesskey();
7660 if (!$this->submissions_open($userid)) {
7661 $notices[] = get_string('duedatereached', 'assign');
7662 return false;
7664 $instance = $this->get_instance();
7666 $data = new stdClass();
7667 $data->userid = $userid;
7668 $mform = new mod_assign_submission_form(null, array($this, $data));
7669 if ($mform->is_cancelled()) {
7670 return true;
7672 if ($data = $mform->get_data()) {
7673 return $this->save_submission($data, $notices);
7675 return false;
7680 * Determine if this users grade can be edited.
7682 * @param int $userid - The student userid
7683 * @param bool $checkworkflow - whether to include a check for the workflow state.
7684 * @param stdClass $gradinginfo - optional, allow gradinginfo to be passed for performance.
7685 * @return bool $gradingdisabled
7687 public function grading_disabled($userid, $checkworkflow = true, $gradinginfo = null) {
7688 if ($checkworkflow && $this->get_instance()->markingworkflow) {
7689 $grade = $this->get_user_grade($userid, false);
7690 $validstates = $this->get_marking_workflow_states_for_current_user();
7691 if (!empty($grade) && !empty($grade->workflowstate) && !array_key_exists($grade->workflowstate, $validstates)) {
7692 return true;
7696 if (is_null($gradinginfo)) {
7697 $gradinginfo = grade_get_grades($this->get_course()->id,
7698 'mod',
7699 'assign',
7700 $this->get_instance()->id,
7701 array($userid));
7704 if (!$gradinginfo) {
7705 return false;
7708 if (!isset($gradinginfo->items[0]->grades[$userid])) {
7709 return false;
7711 $gradingdisabled = $gradinginfo->items[0]->grades[$userid]->locked ||
7712 $gradinginfo->items[0]->grades[$userid]->overridden;
7713 return $gradingdisabled;
7718 * Get an instance of a grading form if advanced grading is enabled.
7719 * This is specific to the assignment, marker and student.
7721 * @param int $userid - The student userid
7722 * @param stdClass|false $grade - The grade record
7723 * @param bool $gradingdisabled
7724 * @return mixed gradingform_instance|null $gradinginstance
7726 protected function get_grading_instance($userid, $grade, $gradingdisabled) {
7727 global $CFG, $USER;
7729 $grademenu = make_grades_menu($this->get_instance()->grade);
7730 $allowgradedecimals = $this->get_instance()->grade > 0;
7732 $advancedgradingwarning = false;
7733 $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
7734 $gradinginstance = null;
7735 if ($gradingmethod = $gradingmanager->get_active_method()) {
7736 $controller = $gradingmanager->get_controller($gradingmethod);
7737 if ($controller->is_form_available()) {
7738 $itemid = null;
7739 if ($grade) {
7740 $itemid = $grade->id;
7742 if ($gradingdisabled && $itemid) {
7743 $gradinginstance = $controller->get_current_instance($USER->id, $itemid);
7744 } else if (!$gradingdisabled) {
7745 $instanceid = optional_param('advancedgradinginstanceid', 0, PARAM_INT);
7746 $gradinginstance = $controller->get_or_create_instance($instanceid,
7747 $USER->id,
7748 $itemid);
7750 } else {
7751 $advancedgradingwarning = $controller->form_unavailable_notification();
7754 if ($gradinginstance) {
7755 $gradinginstance->get_controller()->set_grade_range($grademenu, $allowgradedecimals);
7757 return $gradinginstance;
7761 * Add elements to grade form.
7763 * @param MoodleQuickForm $mform
7764 * @param stdClass $data
7765 * @param array $params
7766 * @return void
7768 public function add_grade_form_elements(MoodleQuickForm $mform, stdClass $data, $params) {
7769 global $USER, $CFG, $SESSION;
7770 $settings = $this->get_instance();
7772 $rownum = isset($params['rownum']) ? $params['rownum'] : 0;
7773 $last = isset($params['last']) ? $params['last'] : true;
7774 $useridlistid = isset($params['useridlistid']) ? $params['useridlistid'] : 0;
7775 $userid = isset($params['userid']) ? $params['userid'] : 0;
7776 $attemptnumber = isset($params['attemptnumber']) ? $params['attemptnumber'] : 0;
7777 $gradingpanel = !empty($params['gradingpanel']);
7778 $bothids = ($userid && $useridlistid);
7780 if (!$userid || $bothids) {
7781 $useridlist = $this->get_grading_userid_list(true, $useridlistid);
7782 } else {
7783 $useridlist = array($userid);
7784 $rownum = 0;
7785 $useridlistid = '';
7788 $userid = $useridlist[$rownum];
7789 // We need to create a grade record matching this attempt number
7790 // or the feedback plugin will have no way to know what is the correct attempt.
7791 $grade = $this->get_user_grade($userid, true, $attemptnumber);
7793 $submission = null;
7794 if ($this->get_instance()->teamsubmission) {
7795 $submission = $this->get_group_submission($userid, 0, false, $attemptnumber);
7796 } else {
7797 $submission = $this->get_user_submission($userid, false, $attemptnumber);
7800 // Add advanced grading.
7801 $gradingdisabled = $this->grading_disabled($userid);
7802 $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
7804 $mform->addElement('header', 'gradeheader', get_string('gradenoun'));
7805 if ($gradinginstance) {
7806 $gradingelement = $mform->addElement('grading',
7807 'advancedgrading',
7808 get_string('gradenoun') . ':',
7809 array('gradinginstance' => $gradinginstance));
7810 if ($gradingdisabled) {
7811 $gradingelement->freeze();
7812 } else {
7813 $mform->addElement('hidden', 'advancedgradinginstanceid', $gradinginstance->get_id());
7814 $mform->setType('advancedgradinginstanceid', PARAM_INT);
7816 } else {
7817 // Use simple direct grading.
7818 if ($this->get_instance()->grade > 0) {
7819 $name = get_string('gradeoutof', 'assign', $this->get_instance()->grade);
7820 if (!$gradingdisabled) {
7821 $gradingelement = $mform->addElement('text', 'grade', $name);
7822 $mform->addHelpButton('grade', 'gradeoutofhelp', 'assign');
7823 $mform->setType('grade', PARAM_RAW);
7824 } else {
7825 $strgradelocked = get_string('gradelocked', 'assign');
7826 $mform->addElement('static', 'gradedisabled', $name, $strgradelocked);
7827 $mform->addHelpButton('gradedisabled', 'gradeoutofhelp', 'assign');
7829 } else {
7830 $grademenu = array(-1 => get_string("nograde")) + make_grades_menu($this->get_instance()->grade);
7831 if (count($grademenu) > 1) {
7832 $gradingelement = $mform->addElement('select', 'grade', get_string('gradenoun') . ':', $grademenu);
7834 // The grade is already formatted with format_float so it needs to be converted back to an integer.
7835 if (!empty($data->grade)) {
7836 $data->grade = (int)unformat_float($data->grade);
7838 $mform->setType('grade', PARAM_INT);
7839 if ($gradingdisabled) {
7840 $gradingelement->freeze();
7846 $gradinginfo = grade_get_grades($this->get_course()->id,
7847 'mod',
7848 'assign',
7849 $this->get_instance()->id,
7850 $userid);
7851 if (!empty($CFG->enableoutcomes)) {
7852 foreach ($gradinginfo->outcomes as $index => $outcome) {
7853 $options = make_grades_menu(-$outcome->scaleid);
7854 $options[0] = get_string('nooutcome', 'grades');
7855 if ($outcome->grades[$userid]->locked) {
7856 $mform->addElement('static',
7857 'outcome_' . $index . '[' . $userid . ']',
7858 $outcome->name . ':',
7859 $options[$outcome->grades[$userid]->grade]);
7860 } else {
7861 $attributes = array('id' => 'menuoutcome_' . $index );
7862 $mform->addElement('select',
7863 'outcome_' . $index . '[' . $userid . ']',
7864 $outcome->name.':',
7865 $options,
7866 $attributes);
7867 $mform->setType('outcome_' . $index . '[' . $userid . ']', PARAM_INT);
7868 $mform->setDefault('outcome_' . $index . '[' . $userid . ']',
7869 $outcome->grades[$userid]->grade);
7874 $capabilitylist = array('gradereport/grader:view', 'moodle/grade:viewall');
7875 $usergrade = get_string('notgraded', 'assign');
7876 if (has_all_capabilities($capabilitylist, $this->get_course_context())) {
7877 $urlparams = array('id'=>$this->get_course()->id);
7878 $url = new moodle_url('/grade/report/grader/index.php', $urlparams);
7879 if (isset($gradinginfo->items[0]->grades[$userid]->grade)) {
7880 $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
7882 $gradestring = $this->get_renderer()->action_link($url, $usergrade);
7883 } else {
7884 if (isset($gradinginfo->items[0]->grades[$userid]) &&
7885 !$gradinginfo->items[0]->grades[$userid]->hidden) {
7886 $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
7888 $gradestring = $usergrade;
7891 if ($this->get_instance()->markingworkflow) {
7892 $states = $this->get_marking_workflow_states_for_current_user();
7893 $options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states;
7894 $select = $mform->addElement('select', 'workflowstate', get_string('markingworkflowstate', 'assign'), $options);
7895 $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign');
7896 if (!empty($data->workflowstate) && !array_key_exists($data->workflowstate, $states)) {
7897 // In a workflow state that user should not be able to change, so freeze workflow selector.
7898 // Have to add the state so it shows in the frozen selector.
7899 $allworkflowstates = $this->get_all_marking_workflow_states();
7900 $select->addOption($allworkflowstates[$data->workflowstate], $data->workflowstate);
7901 $mform->freeze('workflowstate');
7903 $gradingstatus = $this->get_grading_status($userid);
7904 if ($gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
7905 if ($grade->grade && $grade->grade != -1) {
7906 if ($settings->grade > 0) {
7907 $assigngradestring = format_float($grade->grade, $this->get_grade_item()->get_decimals());
7908 } else {
7909 $assigngradestring = make_grades_menu($settings->grade)[grade_floatval($grade->grade)];
7911 $assigngradestring = html_writer::span($assigngradestring, 'currentgrade');
7912 $label = get_string('currentassigngrade', 'assign');
7913 $mform->addElement('static', 'currentassigngrade', $label, $assigngradestring);
7918 if ($this->get_instance()->markingworkflow &&
7919 $this->get_instance()->markingallocation &&
7920 has_capability('mod/assign:manageallocations', $this->context)) {
7922 list($sort, $params) = users_order_by_sql('u');
7923 // Only enrolled users could be assigned as potential markers.
7924 $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
7925 $markerlist = array('' => get_string('choosemarker', 'assign'));
7926 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
7927 foreach ($markers as $marker) {
7928 $markerlist[$marker->id] = fullname($marker, $viewfullnames);
7930 $mform->addElement('select', 'allocatedmarker', get_string('allocatedmarker', 'assign'), $markerlist);
7931 $mform->addHelpButton('allocatedmarker', 'allocatedmarker', 'assign');
7932 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW);
7933 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW);
7934 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE);
7935 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
7938 $gradestring = '<span class="currentgrade">' . $gradestring . '</span>';
7939 $mform->addElement('static', 'currentgrade', get_string('currentgrade', 'assign'), $gradestring);
7941 if (count($useridlist) > 1) {
7942 $strparams = array('current'=>$rownum+1, 'total'=>count($useridlist));
7943 $name = get_string('outof', 'assign', $strparams);
7944 $mform->addElement('static', 'gradingstudent', get_string('gradingstudent', 'assign'), $name);
7947 // Let feedback plugins add elements to the grading form.
7948 $this->add_plugin_grade_elements($grade, $mform, $data, $userid);
7950 // Hidden params.
7951 $mform->addElement('hidden', 'id', $this->get_course_module()->id);
7952 $mform->setType('id', PARAM_INT);
7953 $mform->addElement('hidden', 'rownum', $rownum);
7954 $mform->setType('rownum', PARAM_INT);
7955 $mform->setConstant('rownum', $rownum);
7956 $mform->addElement('hidden', 'useridlistid', $useridlistid);
7957 $mform->setType('useridlistid', PARAM_ALPHANUM);
7958 $mform->addElement('hidden', 'attemptnumber', $attemptnumber);
7959 $mform->setType('attemptnumber', PARAM_INT);
7960 $mform->addElement('hidden', 'ajax', optional_param('ajax', 0, PARAM_INT));
7961 $mform->setType('ajax', PARAM_INT);
7962 $mform->addElement('hidden', 'userid', optional_param('userid', 0, PARAM_INT));
7963 $mform->setType('userid', PARAM_INT);
7965 if ($this->get_instance()->teamsubmission) {
7966 $mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign'));
7967 $mform->addElement('selectyesno', 'applytoall', get_string('applytoteam', 'assign'));
7968 $mform->setDefault('applytoall', 1);
7971 // Do not show if we are editing a previous attempt.
7972 if (($attemptnumber == -1 || ($attemptnumber + 1) == count($this->get_all_submissions($userid))) &&
7973 ($this->get_instance()->maxattempts > 1 || $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS)) {
7974 $mform->addElement('header', 'attemptsettings', get_string('attemptsettings', 'assign'));
7975 $attemptreopenmethod = get_string('attemptreopenmethod_' . $this->get_instance()->attemptreopenmethod, 'assign');
7976 $mform->addElement('static', 'attemptreopenmethod', get_string('attemptreopenmethod', 'assign'), $attemptreopenmethod);
7978 $attemptnumber = 0;
7979 if ($submission) {
7980 $attemptnumber = $submission->attemptnumber;
7982 $maxattempts = $this->get_instance()->maxattempts;
7983 if ($maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) {
7984 $maxattempts = get_string('unlimitedattempts', 'assign');
7986 $mform->addelement('static', 'maxattemptslabel', get_string('maxattempts', 'assign'), $maxattempts);
7987 $mform->addelement('static', 'attemptnumberlabel', get_string('attemptnumber', 'assign'), $attemptnumber + 1);
7989 $ismanual = $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL;
7990 $issubmission = !empty($submission);
7991 $isunlimited = $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS;
7992 $islessthanmaxattempts = $issubmission && ($submission->attemptnumber < ($this->get_instance()->maxattempts-1));
7994 if ($ismanual && (!$issubmission || $isunlimited || $islessthanmaxattempts)) {
7995 $mform->addElement('selectyesno', 'addattempt', get_string('addattempt', 'assign'));
7996 $mform->setDefault('addattempt', 0);
7999 if (!$gradingpanel) {
8000 $mform->addElement('selectyesno', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
8001 } else {
8002 $mform->addElement('hidden', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
8003 $mform->setType('sendstudentnotifications', PARAM_BOOL);
8005 // Get assignment visibility information for student.
8006 $modinfo = get_fast_modinfo($settings->course, $userid);
8007 $cm = $modinfo->get_cm($this->get_course_module()->id);
8009 // Don't allow notification to be sent if the student can't access the assignment,
8010 // or until in "Released" state if using marking workflow.
8011 if (!$cm->uservisible) {
8012 $mform->setDefault('sendstudentnotifications', 0);
8013 $mform->freeze('sendstudentnotifications');
8014 } else if ($this->get_instance()->markingworkflow) {
8015 $mform->setDefault('sendstudentnotifications', 0);
8016 if (!$gradingpanel) {
8017 $mform->disabledIf('sendstudentnotifications', 'workflowstate', 'neq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
8019 } else {
8020 $mform->setDefault('sendstudentnotifications', $this->get_instance()->sendstudentnotifications);
8023 $mform->addElement('hidden', 'action', 'submitgrade');
8024 $mform->setType('action', PARAM_ALPHA);
8026 if (!$gradingpanel) {
8028 $buttonarray = array();
8029 $name = get_string('savechanges', 'assign');
8030 $buttonarray[] = $mform->createElement('submit', 'savegrade', $name);
8031 if (!$last) {
8032 $name = get_string('savenext', 'assign');
8033 $buttonarray[] = $mform->createElement('submit', 'saveandshownext', $name);
8035 $buttonarray[] = $mform->createElement('cancel', 'cancelbutton', get_string('cancel'));
8036 $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
8037 $mform->closeHeaderBefore('buttonar');
8038 $buttonarray = array();
8040 if ($rownum > 0) {
8041 $name = get_string('previous', 'assign');
8042 $buttonarray[] = $mform->createElement('submit', 'nosaveandprevious', $name);
8045 if (!$last) {
8046 $name = get_string('nosavebutnext', 'assign');
8047 $buttonarray[] = $mform->createElement('submit', 'nosaveandnext', $name);
8049 if (!empty($buttonarray)) {
8050 $mform->addGroup($buttonarray, 'navar', '', array(' '), false);
8053 // The grading form does not work well with shortforms.
8054 $mform->setDisableShortforms();
8058 * Add elements in submission plugin form.
8060 * @param mixed $submission stdClass|null
8061 * @param MoodleQuickForm $mform
8062 * @param stdClass $data
8063 * @param int $userid The current userid (same as $USER->id)
8064 * @return void
8066 protected function add_plugin_submission_elements($submission,
8067 MoodleQuickForm $mform,
8068 stdClass $data,
8069 $userid) {
8070 foreach ($this->submissionplugins as $plugin) {
8071 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
8072 $plugin->get_form_elements_for_user($submission, $mform, $data, $userid);
8078 * Check if feedback plugins installed are enabled.
8080 * @return bool
8082 public function is_any_feedback_plugin_enabled() {
8083 if (!isset($this->cache['any_feedback_plugin_enabled'])) {
8084 $this->cache['any_feedback_plugin_enabled'] = false;
8085 foreach ($this->feedbackplugins as $plugin) {
8086 if ($plugin->is_enabled() && $plugin->is_visible()) {
8087 $this->cache['any_feedback_plugin_enabled'] = true;
8088 break;
8093 return $this->cache['any_feedback_plugin_enabled'];
8098 * Check if submission plugins installed are enabled.
8100 * @return bool
8102 public function is_any_submission_plugin_enabled() {
8103 if (!isset($this->cache['any_submission_plugin_enabled'])) {
8104 $this->cache['any_submission_plugin_enabled'] = false;
8105 foreach ($this->submissionplugins as $plugin) {
8106 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
8107 $this->cache['any_submission_plugin_enabled'] = true;
8108 break;
8113 return $this->cache['any_submission_plugin_enabled'];
8118 * Add elements to submission form.
8119 * @param MoodleQuickForm $mform
8120 * @param stdClass $data
8121 * @return void
8123 public function add_submission_form_elements(MoodleQuickForm $mform, stdClass $data) {
8124 global $USER;
8126 $userid = $data->userid;
8127 // Team submissions.
8128 if ($this->get_instance()->teamsubmission) {
8129 $submission = $this->get_group_submission($userid, 0, false);
8130 } else {
8131 $submission = $this->get_user_submission($userid, false);
8134 // Submission statement.
8135 $adminconfig = $this->get_admin_config();
8136 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
8138 $draftsenabled = $this->get_instance()->submissiondrafts;
8139 $submissionstatement = '';
8141 if ($requiresubmissionstatement) {
8142 $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
8145 // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
8146 // that the submission statement checkbox will be displayed.
8147 if (empty($submissionstatement)) {
8148 $requiresubmissionstatement = false;
8151 $mform->addElement('header', 'submission header', get_string('addsubmission', 'mod_assign'));
8153 // Only show submission statement if we are editing our own submission.
8154 if ($requiresubmissionstatement && !$draftsenabled && $userid == $USER->id) {
8155 $mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement);
8156 $mform->addRule('submissionstatement', get_string('submissionstatementrequired', 'mod_assign'),
8157 'required', null, 'client');
8160 $this->add_plugin_submission_elements($submission, $mform, $data, $userid);
8162 // Hidden params.
8163 $mform->addElement('hidden', 'id', $this->get_course_module()->id);
8164 $mform->setType('id', PARAM_INT);
8166 $mform->addElement('hidden', 'userid', $userid);
8167 $mform->setType('userid', PARAM_INT);
8169 $mform->addElement('hidden', 'action', 'savesubmission');
8170 $mform->setType('action', PARAM_ALPHA);
8174 * Remove any data from the current submission.
8176 * @param int $userid
8177 * @return boolean
8178 * @throws coding_exception
8180 public function remove_submission($userid) {
8181 global $USER;
8183 if (!$this->can_edit_submission($userid, $USER->id)) {
8184 $user = core_user::get_user($userid);
8185 $message = get_string('usersubmissioncannotberemoved', 'assign', fullname($user));
8186 $this->set_error_message($message);
8187 return false;
8190 if ($this->get_instance()->teamsubmission) {
8191 $submission = $this->get_group_submission($userid, 0, false);
8192 } else {
8193 $submission = $this->get_user_submission($userid, false);
8196 if (!$submission) {
8197 return false;
8199 $submission->status = $submission->attemptnumber ? ASSIGN_SUBMISSION_STATUS_REOPENED : ASSIGN_SUBMISSION_STATUS_NEW;
8200 $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
8202 // Tell each submission plugin we were saved with no data.
8203 $plugins = $this->get_submission_plugins();
8204 foreach ($plugins as $plugin) {
8205 if ($plugin->is_enabled() && $plugin->is_visible()) {
8206 $plugin->remove($submission);
8210 $completion = new completion_info($this->get_course());
8211 if ($completion->is_enabled($this->get_course_module()) &&
8212 $this->get_instance()->completionsubmit) {
8213 $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
8216 submission_removed::create_from_submission($this, $submission)->trigger();
8217 submission_status_updated::create_from_submission($this, $submission)->trigger();
8218 return true;
8222 * Revert to draft.
8224 * @param int $userid
8225 * @return boolean
8227 public function revert_to_draft($userid) {
8228 global $DB, $USER;
8230 // Need grade permission.
8231 require_capability('mod/assign:grade', $this->context);
8233 if ($this->get_instance()->teamsubmission) {
8234 $submission = $this->get_group_submission($userid, 0, false);
8235 } else {
8236 $submission = $this->get_user_submission($userid, false);
8239 if (!$submission) {
8240 return false;
8242 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
8243 $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
8245 // Give each submission plugin a chance to process the reverting to draft.
8246 $plugins = $this->get_submission_plugins();
8247 foreach ($plugins as $plugin) {
8248 if ($plugin->is_enabled() && $plugin->is_visible()) {
8249 $plugin->revert_to_draft($submission);
8252 // Update the modified time on the grade (grader modified).
8253 $grade = $this->get_user_grade($userid, true);
8254 $grade->grader = $USER->id;
8255 $this->update_grade($grade);
8257 $completion = new completion_info($this->get_course());
8258 if ($completion->is_enabled($this->get_course_module()) &&
8259 $this->get_instance()->completionsubmit) {
8260 $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
8262 \mod_assign\event\submission_status_updated::create_from_submission($this, $submission)->trigger();
8263 return true;
8267 * Remove the current submission.
8269 * @param int $userid
8270 * @return boolean
8272 protected function process_remove_submission($userid = 0) {
8273 require_sesskey();
8275 if (!$userid) {
8276 $userid = required_param('userid', PARAM_INT);
8279 return $this->remove_submission($userid);
8283 * Revert to draft.
8284 * Uses url parameter userid if userid not supplied as a parameter.
8286 * @param int $userid
8287 * @return boolean
8289 protected function process_revert_to_draft($userid = 0) {
8290 require_sesskey();
8292 if (!$userid) {
8293 $userid = required_param('userid', PARAM_INT);
8296 return $this->revert_to_draft($userid);
8300 * Prevent student updates to this submission
8302 * @param int $userid
8303 * @return bool
8305 public function lock_submission($userid) {
8306 global $USER, $DB;
8307 // Need grade permission.
8308 require_capability('mod/assign:grade', $this->context);
8310 // Give each submission plugin a chance to process the locking.
8311 $plugins = $this->get_submission_plugins();
8312 $submission = $this->get_user_submission($userid, false);
8314 $flags = $this->get_user_flags($userid, true);
8315 $flags->locked = 1;
8316 $this->update_user_flags($flags);
8318 foreach ($plugins as $plugin) {
8319 if ($plugin->is_enabled() && $plugin->is_visible()) {
8320 $plugin->lock($submission, $flags);
8324 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8325 \mod_assign\event\submission_locked::create_from_user($this, $user)->trigger();
8326 return true;
8331 * Set the workflow state for multiple users
8333 * @return void
8335 protected function process_set_batch_marking_workflow_state() {
8336 global $CFG, $DB;
8338 // Include batch marking workflow form.
8339 require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
8341 $formparams = array(
8342 'userscount' => 0, // This form is never re-displayed, so we don't need to
8343 'usershtml' => '', // initialise these parameters with real information.
8344 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
8347 $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
8349 if ($mform->is_cancelled()) {
8350 return true;
8353 if ($formdata = $mform->get_data()) {
8354 $useridlist = explode(',', $formdata->selectedusers);
8355 $state = $formdata->markingworkflowstate;
8357 foreach ($useridlist as $userid) {
8358 $flags = $this->get_user_flags($userid, true);
8360 $flags->workflowstate = $state;
8362 // Clear the mailed flag if notification is requested, the student hasn't been
8363 // notified previously, the student can access the assignment, and the state
8364 // is "Released".
8365 $modinfo = get_fast_modinfo($this->course, $userid);
8366 $cm = $modinfo->get_cm($this->get_course_module()->id);
8367 if ($formdata->sendstudentnotifications && $cm->uservisible &&
8368 $state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8369 $flags->mailed = 0;
8372 $gradingdisabled = $this->grading_disabled($userid);
8374 // Will not apply update if user does not have permission to assign this workflow state.
8375 if (!$gradingdisabled && $this->update_user_flags($flags)) {
8376 // Update Gradebook.
8377 $grade = $this->get_user_grade($userid, true);
8378 // Fetch any feedback for this student.
8379 $gradebookplugin = $this->get_admin_config()->feedback_plugin_for_gradebook;
8380 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
8381 $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
8382 if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
8383 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
8384 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
8385 $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
8387 $this->update_grade($grade);
8388 $assign = clone $this->get_instance();
8389 $assign->cmidnumber = $this->get_course_module()->idnumber;
8390 // Set assign gradebook feedback plugin status.
8391 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
8393 // If markinganonymous is enabled then allow to release grades anonymously.
8394 if (isset($assign->markinganonymous) && $assign->markinganonymous == 1) {
8395 assign_update_grades($assign, $userid);
8397 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8398 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $state)->trigger();
8405 * Set the marking allocation for multiple users
8407 * @return void
8409 protected function process_set_batch_marking_allocation() {
8410 global $CFG, $DB;
8412 // Include batch marking allocation form.
8413 require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
8415 $formparams = array(
8416 'userscount' => 0, // This form is never re-displayed, so we don't need to
8417 'usershtml' => '' // initialise these parameters with real information.
8420 list($sort, $params) = users_order_by_sql('u');
8421 // Only enrolled users could be assigned as potential markers.
8422 $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
8423 $markerlist = array();
8424 foreach ($markers as $marker) {
8425 $markerlist[$marker->id] = fullname($marker);
8428 $formparams['markers'] = $markerlist;
8430 $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
8432 if ($mform->is_cancelled()) {
8433 return true;
8436 if ($formdata = $mform->get_data()) {
8437 $useridlist = explode(',', $formdata->selectedusers);
8438 $marker = $DB->get_record('user', array('id' => $formdata->allocatedmarker), '*', MUST_EXIST);
8440 foreach ($useridlist as $userid) {
8441 $flags = $this->get_user_flags($userid, true);
8442 if ($flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW ||
8443 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW ||
8444 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE ||
8445 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8447 continue; // Allocated marker can only be changed in certain workflow states.
8450 $flags->allocatedmarker = $marker->id;
8452 if ($this->update_user_flags($flags)) {
8453 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8454 \mod_assign\event\marker_updated::create_from_marker($this, $user, $marker)->trigger();
8462 * Prevent student updates to this submission.
8463 * Uses url parameter userid.
8465 * @param int $userid
8466 * @return void
8468 protected function process_lock_submission($userid = 0) {
8470 require_sesskey();
8472 if (!$userid) {
8473 $userid = required_param('userid', PARAM_INT);
8476 return $this->lock_submission($userid);
8480 * Unlock the student submission.
8482 * @param int $userid
8483 * @return bool
8485 public function unlock_submission($userid) {
8486 global $USER, $DB;
8488 // Need grade permission.
8489 require_capability('mod/assign:grade', $this->context);
8491 // Give each submission plugin a chance to process the unlocking.
8492 $plugins = $this->get_submission_plugins();
8493 $submission = $this->get_user_submission($userid, false);
8495 $flags = $this->get_user_flags($userid, true);
8496 $flags->locked = 0;
8497 $this->update_user_flags($flags);
8499 foreach ($plugins as $plugin) {
8500 if ($plugin->is_enabled() && $plugin->is_visible()) {
8501 $plugin->unlock($submission, $flags);
8505 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8506 \mod_assign\event\submission_unlocked::create_from_user($this, $user)->trigger();
8507 return true;
8511 * Unlock the student submission.
8512 * Uses url parameter userid.
8514 * @param int $userid
8515 * @return bool
8517 protected function process_unlock_submission($userid = 0) {
8519 require_sesskey();
8521 if (!$userid) {
8522 $userid = required_param('userid', PARAM_INT);
8525 return $this->unlock_submission($userid);
8529 * Apply a grade from a grading form to a user (may be called multiple times for a group submission).
8531 * @param stdClass $formdata - the data from the form
8532 * @param int $userid - the user to apply the grade to
8533 * @param int $attemptnumber - The attempt number to apply the grade to.
8534 * @return void
8536 protected function apply_grade_to_user($formdata, $userid, $attemptnumber) {
8537 global $USER, $CFG, $DB;
8539 $grade = $this->get_user_grade($userid, true, $attemptnumber);
8540 $originalgrade = $grade->grade;
8541 $gradingdisabled = $this->grading_disabled($userid);
8542 $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
8543 if (!$gradingdisabled) {
8544 if ($gradinginstance) {
8545 $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading,
8546 $grade->id);
8547 } else {
8548 // Handle the case when grade is set to No Grade.
8549 if (isset($formdata->grade)) {
8550 $grade->grade = grade_floatval(unformat_float($formdata->grade));
8553 if (isset($formdata->workflowstate) || isset($formdata->allocatedmarker)) {
8554 $flags = $this->get_user_flags($userid, true);
8555 $oldworkflowstate = $flags->workflowstate;
8556 $flags->workflowstate = isset($formdata->workflowstate) ? $formdata->workflowstate : $flags->workflowstate;
8557 $flags->allocatedmarker = isset($formdata->allocatedmarker) ? $formdata->allocatedmarker : $flags->allocatedmarker;
8558 if ($this->update_user_flags($flags) &&
8559 isset($formdata->workflowstate) &&
8560 $formdata->workflowstate !== $oldworkflowstate) {
8561 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8562 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $formdata->workflowstate)->trigger();
8566 $grade->grader= $USER->id;
8568 $adminconfig = $this->get_admin_config();
8569 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
8571 $feedbackmodified = false;
8573 // Call save in plugins.
8574 foreach ($this->feedbackplugins as $plugin) {
8575 if ($plugin->is_enabled() && $plugin->is_visible()) {
8576 $gradingmodified = $plugin->is_feedback_modified($grade, $formdata);
8577 if ($gradingmodified) {
8578 if (!$plugin->save($grade, $formdata)) {
8579 $result = false;
8580 throw new \moodle_exception($plugin->get_error());
8582 // If $feedbackmodified is true, keep it true.
8583 $feedbackmodified = $feedbackmodified || $gradingmodified;
8585 if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
8586 // This is the feedback plugin chose to push comments to the gradebook.
8587 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
8588 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
8589 $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
8594 // We do not want to update the timemodified if no grade was added.
8595 if (!empty($formdata->addattempt) ||
8596 ($originalgrade !== null && $originalgrade != -1) ||
8597 ($grade->grade !== null && $grade->grade != -1) ||
8598 $feedbackmodified) {
8599 $this->update_grade($grade, !empty($formdata->addattempt));
8602 // We never send notifications if we have marking workflow and the grade is not released.
8603 if ($this->get_instance()->markingworkflow &&
8604 isset($formdata->workflowstate) &&
8605 $formdata->workflowstate != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8606 $formdata->sendstudentnotifications = false;
8609 // Note the default if not provided for this option is true (e.g. webservices).
8610 // This is for backwards compatibility.
8611 if (!isset($formdata->sendstudentnotifications) || $formdata->sendstudentnotifications) {
8612 $this->notify_grade_modified($grade, true);
8618 * Save outcomes submitted from grading form.
8620 * @param int $userid
8621 * @param stdClass $formdata
8622 * @param int $sourceuserid The user ID under which the outcome data is accessible. This is relevant
8623 * for an outcome set to a user but applied to an entire group.
8625 protected function process_outcomes($userid, $formdata, $sourceuserid = null) {
8626 global $CFG, $USER;
8628 if (empty($CFG->enableoutcomes)) {
8629 return;
8631 if ($this->grading_disabled($userid)) {
8632 return;
8635 require_once($CFG->libdir.'/gradelib.php');
8637 $data = array();
8638 $gradinginfo = grade_get_grades($this->get_course()->id,
8639 'mod',
8640 'assign',
8641 $this->get_instance()->id,
8642 $userid);
8644 if (!empty($gradinginfo->outcomes)) {
8645 foreach ($gradinginfo->outcomes as $index => $oldoutcome) {
8646 $name = 'outcome_'.$index;
8647 $sourceuserid = $sourceuserid !== null ? $sourceuserid : $userid;
8648 if (isset($formdata->{$name}[$sourceuserid]) &&
8649 $oldoutcome->grades[$userid]->grade != $formdata->{$name}[$sourceuserid]) {
8650 $data[$index] = $formdata->{$name}[$sourceuserid];
8654 if (count($data) > 0) {
8655 grade_update_outcomes('mod/assign',
8656 $this->course->id,
8657 'mod',
8658 'assign',
8659 $this->get_instance()->id,
8660 $userid,
8661 $data);
8666 * If the requirements are met - reopen the submission for another attempt.
8667 * Only call this function when grading the latest attempt.
8669 * @param int $userid The userid.
8670 * @param stdClass $submission The submission (may be a group submission).
8671 * @param bool $addattempt - True if the "allow another attempt" checkbox was checked.
8672 * @return bool - true if another attempt was added.
8674 protected function reopen_submission_if_required($userid, $submission, $addattempt) {
8675 $instance = $this->get_instance();
8676 $maxattemptsreached = !empty($submission) &&
8677 $submission->attemptnumber >= ($instance->maxattempts - 1) &&
8678 $instance->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS;
8679 $shouldreopen = false;
8680 switch ($instance->attemptreopenmethod) {
8681 case ASSIGN_ATTEMPT_REOPEN_METHOD_AUTOMATIC:
8682 $shouldreopen = true;
8683 break;
8684 case ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS:
8685 // Check the gradetopass from the gradebook.
8686 $gradeitem = $this->get_grade_item();
8687 if ($gradeitem) {
8688 $gradegrade = grade_grade::fetch(['userid' => $userid, 'itemid' => $gradeitem->id]);
8690 // Do not reopen if is_passed returns null, e.g. if there is no pass criterion set.
8691 if ($gradegrade && ($gradegrade->is_passed() === false)) {
8692 $shouldreopen = true;
8695 break;
8696 case ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL:
8697 if (!empty($addattempt)) {
8698 $shouldreopen = true;
8700 break;
8702 if ($shouldreopen && !$maxattemptsreached) {
8703 $this->add_attempt($userid);
8704 return true;
8706 return false;
8710 * Save grade update.
8712 * @param int $userid
8713 * @param stdClass $data
8714 * @return bool - was the grade saved
8716 public function save_grade($userid, $data) {
8718 // Need grade permission.
8719 require_capability('mod/assign:grade', $this->context);
8721 $instance = $this->get_instance();
8722 $submission = null;
8723 if ($instance->teamsubmission) {
8724 // We need to know what the most recent group submission is.
8725 // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
8726 // and when deciding if we need to update the gradebook with an edited grade.
8727 $mostrecentsubmission = $this->get_group_submission($userid, 0, false, -1);
8728 $this->set_most_recent_team_submission($mostrecentsubmission);
8729 // Get the submission that we are saving grades for. The data attempt number determines which submission attempt.
8730 $submission = $this->get_group_submission($userid, 0, false, $data->attemptnumber);
8731 } else {
8732 $submission = $this->get_user_submission($userid, false, $data->attemptnumber);
8734 if ($instance->teamsubmission && !empty($data->applytoall)) {
8735 $groupid = 0;
8736 if ($this->get_submission_group($userid)) {
8737 $group = $this->get_submission_group($userid);
8738 if ($group) {
8739 $groupid = $group->id;
8742 $members = $this->get_submission_group_members($groupid, true, $this->show_only_active_users());
8743 foreach ($members as $member) {
8744 // We only want to update the grade for this group submission attempt. The data attempt number could be
8745 // -1 which may end up in additional attempts being created for each group member instead of just one
8746 // additional attempt for the group.
8747 $this->apply_grade_to_user($data, $member->id, $submission->attemptnumber);
8748 $this->process_outcomes($member->id, $data, $userid);
8750 } else {
8751 $this->apply_grade_to_user($data, $userid, $data->attemptnumber);
8753 $this->process_outcomes($userid, $data);
8756 return true;
8760 * Save grade.
8762 * @param moodleform $mform
8763 * @return bool - was the grade saved
8765 protected function process_save_grade(&$mform) {
8766 global $CFG, $SESSION;
8767 // Include grade form.
8768 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
8770 require_sesskey();
8772 $instance = $this->get_instance();
8773 $rownum = required_param('rownum', PARAM_INT);
8774 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
8775 $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
8776 $userid = optional_param('userid', 0, PARAM_INT);
8777 if (!$userid) {
8778 if (empty($SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)])) {
8779 // If the userid list is not stored we must not save, as it is possible that the user in a
8780 // given row position may not be the same now as when the grading page was generated.
8781 $url = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
8782 throw new moodle_exception('useridlistnotcached', 'mod_assign', $url);
8784 $useridlist = $SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)];
8785 } else {
8786 $useridlist = array($userid);
8787 $rownum = 0;
8790 $last = false;
8791 $userid = $useridlist[$rownum];
8792 if ($rownum == count($useridlist) - 1) {
8793 $last = true;
8796 $data = new stdClass();
8798 $gradeformparams = array('rownum' => $rownum,
8799 'useridlistid' => $useridlistid,
8800 'last' => $last,
8801 'attemptnumber' => $attemptnumber,
8802 'userid' => $userid);
8803 $mform = new mod_assign_grade_form(null,
8804 array($this, $data, $gradeformparams),
8805 'post',
8807 array('class'=>'gradeform'));
8809 if ($formdata = $mform->get_data()) {
8810 return $this->save_grade($userid, $formdata);
8811 } else {
8812 return false;
8817 * This function is a static wrapper around can_upgrade.
8819 * @param string $type The plugin type
8820 * @param int $version The plugin version
8821 * @return bool
8823 public static function can_upgrade_assignment($type, $version) {
8824 $assignment = new assign(null, null, null);
8825 return $assignment->can_upgrade($type, $version);
8829 * This function returns true if it can upgrade an assignment from the 2.2 module.
8831 * @param string $type The plugin type
8832 * @param int $version The plugin version
8833 * @return bool
8835 public function can_upgrade($type, $version) {
8836 if ($type == 'offline' && $version >= 2011112900) {
8837 return true;
8839 foreach ($this->submissionplugins as $plugin) {
8840 if ($plugin->can_upgrade($type, $version)) {
8841 return true;
8844 foreach ($this->feedbackplugins as $plugin) {
8845 if ($plugin->can_upgrade($type, $version)) {
8846 return true;
8849 return false;
8853 * Copy all the files from the old assignment files area to the new one.
8854 * This is used by the plugin upgrade code.
8856 * @param int $oldcontextid The old assignment context id
8857 * @param int $oldcomponent The old assignment component ('assignment')
8858 * @param int $oldfilearea The old assignment filearea ('submissions')
8859 * @param int $olditemid The old submissionid (can be null e.g. intro)
8860 * @param int $newcontextid The new assignment context id
8861 * @param int $newcomponent The new assignment component ('assignment')
8862 * @param int $newfilearea The new assignment filearea ('submissions')
8863 * @param int $newitemid The new submissionid (can be null e.g. intro)
8864 * @return int The number of files copied
8866 public function copy_area_files_for_upgrade($oldcontextid,
8867 $oldcomponent,
8868 $oldfilearea,
8869 $olditemid,
8870 $newcontextid,
8871 $newcomponent,
8872 $newfilearea,
8873 $newitemid) {
8874 // Note, this code is based on some code in filestorage - but that code
8875 // deleted the old files (which we don't want).
8876 $count = 0;
8878 $fs = get_file_storage();
8880 $oldfiles = $fs->get_area_files($oldcontextid,
8881 $oldcomponent,
8882 $oldfilearea,
8883 $olditemid,
8884 'id',
8885 false);
8886 foreach ($oldfiles as $oldfile) {
8887 $filerecord = new stdClass();
8888 $filerecord->contextid = $newcontextid;
8889 $filerecord->component = $newcomponent;
8890 $filerecord->filearea = $newfilearea;
8891 $filerecord->itemid = $newitemid;
8892 $fs->create_file_from_storedfile($filerecord, $oldfile);
8893 $count += 1;
8896 return $count;
8900 * Add a new attempt for each user in the list - but reopen each group assignment
8901 * at most 1 time.
8903 * @param array $useridlist Array of userids to reopen.
8904 * @return bool
8906 protected function process_add_attempt_group($useridlist) {
8907 $groupsprocessed = array();
8908 $result = true;
8910 foreach ($useridlist as $userid) {
8911 $groupid = 0;
8912 $group = $this->get_submission_group($userid);
8913 if ($group) {
8914 $groupid = $group->id;
8917 if (empty($groupsprocessed[$groupid])) {
8918 // We need to know what the most recent group submission is.
8919 // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
8920 // and when deciding if we need to update the gradebook with an edited grade.
8921 $currentsubmission = $this->get_group_submission($userid, 0, false, -1);
8922 $this->set_most_recent_team_submission($currentsubmission);
8923 $result = $this->process_add_attempt($userid) && $result;
8924 $groupsprocessed[$groupid] = true;
8927 return $result;
8931 * Check for a sess key and then call add_attempt.
8933 * @param int $userid int The user to add the attempt for
8934 * @return bool - true if successful.
8936 protected function process_add_attempt($userid) {
8937 require_sesskey();
8939 return $this->add_attempt($userid);
8943 * Add a new attempt for a user.
8945 * @param int $userid int The user to add the attempt for
8946 * @return bool - true if successful.
8948 protected function add_attempt($userid) {
8949 require_capability('mod/assign:grade', $this->context);
8951 // If additional attempts are disallowed.
8952 if ($this->get_instance()->maxattempts == 1) {
8953 return false;
8956 if ($this->get_instance()->teamsubmission) {
8957 $oldsubmission = $this->get_group_submission($userid, 0, false);
8958 } else {
8959 $oldsubmission = $this->get_user_submission($userid, false);
8962 if (!$oldsubmission) {
8963 return false;
8966 // No more than max attempts allowed.
8967 if ($this->get_instance()->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS &&
8968 $oldsubmission->attemptnumber >= ($this->get_instance()->maxattempts - 1)) {
8969 return false;
8972 // Create the new submission record for the group/user.
8973 if ($this->get_instance()->teamsubmission) {
8974 if (isset($this->mostrecentteamsubmission)) {
8975 // Team submissions can end up in this function for each user (via save_grade). We don't want to create
8976 // more than one attempt for the whole team.
8977 if ($this->mostrecentteamsubmission->attemptnumber == $oldsubmission->attemptnumber) {
8978 $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
8979 } else {
8980 $newsubmission = $this->get_group_submission($userid, 0, false, $oldsubmission->attemptnumber);
8982 } else {
8983 debugging('Please use set_most_recent_team_submission() before calling add_attempt', DEBUG_DEVELOPER);
8984 $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
8986 } else {
8987 $newsubmission = $this->get_user_submission($userid, true, $oldsubmission->attemptnumber + 1);
8990 // Set the status of the new attempt to reopened.
8991 $newsubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
8993 // Give each submission plugin a chance to process the add_attempt.
8994 $plugins = $this->get_submission_plugins();
8995 foreach ($plugins as $plugin) {
8996 if ($plugin->is_enabled() && $plugin->is_visible()) {
8997 $plugin->add_attempt($oldsubmission, $newsubmission);
9001 $this->update_submission($newsubmission, $userid, false, $this->get_instance()->teamsubmission);
9002 $flags = $this->get_user_flags($userid, false);
9003 if (isset($flags->locked) && $flags->locked) { // May not exist.
9004 $this->process_unlock_submission($userid);
9006 return true;
9010 * Get an upto date list of user grades and feedback for the gradebook.
9012 * @param int $userid int or 0 for all users
9013 * @return array of grade data formated for the gradebook api
9014 * The data required by the gradebook api is userid,
9015 * rawgrade,
9016 * feedback,
9017 * feedbackformat,
9018 * usermodified,
9019 * dategraded,
9020 * datesubmitted
9022 public function get_user_grades_for_gradebook($userid) {
9023 global $DB, $CFG;
9024 $grades = array();
9025 $assignmentid = $this->get_instance()->id;
9027 $adminconfig = $this->get_admin_config();
9028 $gradebookpluginname = $adminconfig->feedback_plugin_for_gradebook;
9029 $gradebookplugin = null;
9031 // Find the gradebook plugin.
9032 foreach ($this->feedbackplugins as $plugin) {
9033 if ($plugin->is_enabled() && $plugin->is_visible()) {
9034 if (('assignfeedback_' . $plugin->get_type()) == $gradebookpluginname) {
9035 $gradebookplugin = $plugin;
9039 if ($userid) {
9040 $where = ' WHERE u.id = :userid ';
9041 } else {
9042 $where = ' WHERE u.id != :userid ';
9045 // When the gradebook asks us for grades - only return the last attempt for each user.
9046 $params = array('assignid1'=>$assignmentid,
9047 'assignid2'=>$assignmentid,
9048 'userid'=>$userid);
9049 $graderesults = $DB->get_recordset_sql('SELECT
9050 u.id as userid,
9051 s.timemodified as datesubmitted,
9052 g.grade as rawgrade,
9053 g.timemodified as dategraded,
9054 g.grader as usermodified
9055 FROM {user} u
9056 LEFT JOIN {assign_submission} s
9057 ON u.id = s.userid and s.assignment = :assignid1 AND
9058 s.latest = 1
9059 JOIN {assign_grades} g
9060 ON u.id = g.userid and g.assignment = :assignid2 AND
9061 g.attemptnumber = s.attemptnumber' .
9062 $where, $params);
9064 foreach ($graderesults as $result) {
9065 $gradingstatus = $this->get_grading_status($result->userid);
9066 if (!$this->get_instance()->markingworkflow || $gradingstatus == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
9067 $gradebookgrade = clone $result;
9068 // Now get the feedback.
9069 if ($gradebookplugin) {
9070 $grade = $this->get_user_grade($result->userid, false);
9071 if ($grade) {
9072 $feedbacktext = $gradebookplugin->text_for_gradebook($grade);
9073 if (!empty($feedbacktext)) {
9074 $gradebookgrade->feedback = $feedbacktext;
9076 $gradebookgrade->feedbackformat = $gradebookplugin->format_for_gradebook($grade);
9077 $gradebookgrade->feedbackfiles = $gradebookplugin->files_for_gradebook($grade);
9080 $grades[$gradebookgrade->userid] = $gradebookgrade;
9084 $graderesults->close();
9085 return $grades;
9089 * Call the static version of this function
9091 * @param int $userid The userid to lookup
9092 * @return int The unique id
9094 public function get_uniqueid_for_user($userid) {
9095 return self::get_uniqueid_for_user_static($this->get_instance()->id, $userid);
9099 * Foreach participant in the course - assign them a random id.
9101 * @param int $assignid The assignid to lookup
9103 public static function allocate_unique_ids($assignid) {
9104 global $DB;
9106 $cm = get_coursemodule_from_instance('assign', $assignid, 0, false, MUST_EXIST);
9107 $context = context_module::instance($cm->id);
9109 $currentgroup = groups_get_activity_group($cm, true);
9110 $users = get_enrolled_users($context, "mod/assign:submit", $currentgroup, 'u.id');
9112 // Shuffle the users.
9113 shuffle($users);
9115 foreach ($users as $user) {
9116 $record = $DB->get_record('assign_user_mapping',
9117 array('assignment'=>$assignid, 'userid'=>$user->id),
9118 'id');
9119 if (!$record) {
9120 $record = new stdClass();
9121 $record->assignment = $assignid;
9122 $record->userid = $user->id;
9123 $DB->insert_record('assign_user_mapping', $record);
9129 * Lookup this user id and return the unique id for this assignment.
9131 * @param int $assignid The assignment id
9132 * @param int $userid The userid to lookup
9133 * @return int The unique id
9135 public static function get_uniqueid_for_user_static($assignid, $userid) {
9136 global $DB;
9138 // Search for a record.
9139 $params = array('assignment'=>$assignid, 'userid'=>$userid);
9140 if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
9141 return $record->id;
9144 // Be a little smart about this - there is no record for the current user.
9145 // We should ensure any unallocated ids for the current participant
9146 // list are distrubited randomly.
9147 self::allocate_unique_ids($assignid);
9149 // Retry the search for a record.
9150 if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
9151 return $record->id;
9154 // The requested user must not be a participant. Add a record anyway.
9155 $record = new stdClass();
9156 $record->assignment = $assignid;
9157 $record->userid = $userid;
9159 return $DB->insert_record('assign_user_mapping', $record);
9163 * Call the static version of this function.
9165 * @param int $uniqueid The uniqueid to lookup
9166 * @return int The user id or false if they don't exist
9168 public function get_user_id_for_uniqueid($uniqueid) {
9169 return self::get_user_id_for_uniqueid_static($this->get_instance()->id, $uniqueid);
9173 * Lookup this unique id and return the user id for this assignment.
9175 * @param int $assignid The id of the assignment this user mapping is in
9176 * @param int $uniqueid The uniqueid to lookup
9177 * @return int The user id or false if they don't exist
9179 public static function get_user_id_for_uniqueid_static($assignid, $uniqueid) {
9180 global $DB;
9182 // Search for a record.
9183 if ($record = $DB->get_record('assign_user_mapping',
9184 array('assignment'=>$assignid, 'id'=>$uniqueid),
9185 'userid',
9186 IGNORE_MISSING)) {
9187 return $record->userid;
9190 return false;
9194 * Get the list of marking_workflow states the current user has permission to transition a grade to.
9196 * @return array of state => description
9198 public function get_marking_workflow_states_for_current_user() {
9199 if (!empty($this->markingworkflowstates)) {
9200 return $this->markingworkflowstates;
9202 $states = array();
9203 if (has_capability('mod/assign:grade', $this->context)) {
9204 $states[ASSIGN_MARKING_WORKFLOW_STATE_INMARKING] = get_string('markingworkflowstateinmarking', 'assign');
9205 $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW] = get_string('markingworkflowstatereadyforreview', 'assign');
9207 if (has_any_capability(array('mod/assign:reviewgrades',
9208 'mod/assign:managegrades'), $this->context)) {
9209 $states[ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW] = get_string('markingworkflowstateinreview', 'assign');
9210 $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE] = get_string('markingworkflowstatereadyforrelease', 'assign');
9212 if (has_any_capability(array('mod/assign:releasegrades',
9213 'mod/assign:managegrades'), $this->context)) {
9214 $states[ASSIGN_MARKING_WORKFLOW_STATE_RELEASED] = get_string('markingworkflowstatereleased', 'assign');
9216 $this->markingworkflowstates = $states;
9217 return $this->markingworkflowstates;
9221 * Get the list of marking_workflow states.
9223 * @return array Array of multiple state => description.
9225 public function get_all_marking_workflow_states(): array {
9226 if (!empty($this->allmarkingworkflowstates)) {
9227 return $this->allmarkingworkflowstates;
9230 $this->allmarkingworkflowstates = [
9231 ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED => get_string('markingworkflowstatenotmarked', 'assign'),
9232 ASSIGN_MARKING_WORKFLOW_STATE_INMARKING => get_string('markingworkflowstateinmarking', 'assign'),
9233 ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW => get_string('markingworkflowstatereadyforreview', 'assign'),
9234 ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW => get_string('markingworkflowstateinreview', 'assign'),
9235 ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE => get_string('markingworkflowstatereadyforrelease', 'assign'),
9236 ASSIGN_MARKING_WORKFLOW_STATE_RELEASED => get_string('markingworkflowstatereleased', 'assign'),
9239 return $this->allmarkingworkflowstates;
9243 * Check is only active users in course should be shown.
9245 * @return bool true if only active users should be shown.
9247 public function show_only_active_users() {
9248 global $CFG;
9250 if (is_null($this->showonlyactiveenrol)) {
9251 $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
9252 $this->showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
9254 if (!is_null($this->context)) {
9255 $this->showonlyactiveenrol = $this->showonlyactiveenrol ||
9256 !has_capability('moodle/course:viewsuspendedusers', $this->context);
9259 return $this->showonlyactiveenrol;
9263 * Return true is user is active user in course else false
9265 * @param int $userid
9266 * @return bool true is user is active in course.
9268 public function is_active_user($userid) {
9269 return !in_array($userid, get_suspended_userids($this->context, true));
9273 * Returns true if gradebook feedback plugin is enabled
9275 * @return bool true if gradebook feedback plugin is enabled and visible else false.
9277 public function is_gradebook_feedback_enabled() {
9278 // Get default grade book feedback plugin.
9279 $adminconfig = $this->get_admin_config();
9280 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
9281 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
9283 // Check if default gradebook feedback is visible and enabled.
9284 $gradebookfeedbackplugin = $this->get_feedback_plugin_by_type($gradebookplugin);
9286 if (empty($gradebookfeedbackplugin)) {
9287 return false;
9290 if ($gradebookfeedbackplugin->is_visible() && $gradebookfeedbackplugin->is_enabled()) {
9291 return true;
9294 // Gradebook feedback plugin is either not visible/enabled.
9295 return false;
9299 * Returns the grading status.
9301 * @param int $userid the user id
9302 * @return string returns the grading status
9304 public function get_grading_status($userid) {
9305 if ($this->get_instance()->markingworkflow) {
9306 $flags = $this->get_user_flags($userid, false);
9307 if (!empty($flags->workflowstate)) {
9308 return $flags->workflowstate;
9310 return ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED;
9311 } else {
9312 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
9313 $grade = $this->get_user_grade($userid, false, $attemptnumber);
9315 if (!empty($grade) && $grade->grade !== null && $grade->grade >= 0) {
9316 return ASSIGN_GRADING_STATUS_GRADED;
9317 } else {
9318 return ASSIGN_GRADING_STATUS_NOT_GRADED;
9324 * The id used to uniquily identify the cache for this instance of the assign object.
9326 * @return string
9328 public function get_useridlist_key_id() {
9329 return $this->useridlistid;
9333 * Generates the key that should be used for an entry in the useridlist cache.
9335 * @param string $id Generate a key for this instance (optional)
9336 * @return string The key for the id, or new entry if no $id is passed.
9338 public function get_useridlist_key($id = null) {
9339 global $SESSION;
9341 // Ensure the user id list cache is initialised.
9342 if (!isset($SESSION->mod_assign_useridlist)) {
9343 $SESSION->mod_assign_useridlist = [];
9346 if ($id === null) {
9347 $id = $this->get_useridlist_key_id();
9349 return $this->get_course_module()->id . '_' . $id;
9353 * Updates and creates the completion records in mdl_course_modules_completion.
9355 * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity
9356 * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit
9357 * @param obj $submission the submission
9358 * @param int $userid the user id
9359 * @param int $complete
9360 * @param obj $completion
9362 * @return null
9364 protected function update_activity_completion_records($teamsubmission,
9365 $requireallteammemberssubmit,
9366 $submission,
9367 $userid,
9368 $complete,
9369 $completion) {
9371 if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) ||
9372 ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit &&
9373 $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
9375 $members = groups_get_members($submission->groupid);
9377 foreach ($members as $member) {
9378 $completion->update_state($this->get_course_module(), $complete, $member->id);
9380 } else {
9381 $completion->update_state($this->get_course_module(), $complete, $userid);
9384 return;
9388 * Update the module completion status (set it viewed) and trigger module viewed event.
9390 * @since Moodle 3.2
9392 public function set_module_viewed() {
9393 $completion = new completion_info($this->get_course());
9394 $completion->set_module_viewed($this->get_course_module());
9396 // Trigger the course module viewed event.
9397 $assigninstance = $this->get_instance();
9398 $params = [
9399 'objectid' => $assigninstance->id,
9400 'context' => $this->get_context()
9402 if ($this->is_blind_marking()) {
9403 $params['anonymous'] = 1;
9406 $event = \mod_assign\event\course_module_viewed::create($params);
9408 $event->add_record_snapshot('assign', $assigninstance);
9409 $event->trigger();
9413 * Checks for any grade notices, and adds notifications. Will display on assignment main page and grading table.
9415 * @return void The notifications API will render the notifications at the appropriate part of the page.
9417 protected function add_grade_notices() {
9418 if (has_capability('mod/assign:grade', $this->get_context()) && get_config('assign', 'has_rescaled_null_grades_' . $this->get_instance()->id)) {
9419 $link = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades'));
9420 \core\notification::warning(get_string('fixrescalednullgrades', 'mod_assign', ['link' => $link->out()]));
9425 * View fix rescaled null grades.
9427 * @return bool True if null all grades are now fixed.
9429 protected function fix_null_grades() {
9430 global $DB;
9431 $result = $DB->set_field_select(
9432 'assign_grades',
9433 'grade',
9434 ASSIGN_GRADE_NOT_SET,
9435 'grade <> ? AND grade < 0',
9436 [ASSIGN_GRADE_NOT_SET]
9438 $assign = clone $this->get_instance();
9439 $assign->cmidnumber = $this->get_course_module()->idnumber;
9440 assign_update_grades($assign);
9441 return $result;
9445 * View fix rescaled null grades.
9447 * @return void The notifications API will render the notifications at the appropriate part of the page.
9449 protected function view_fix_rescaled_null_grades() {
9450 global $OUTPUT;
9452 $o = '';
9454 require_capability('mod/assign:grade', $this->get_context());
9456 $instance = $this->get_instance();
9458 $o .= $this->get_renderer()->render(
9459 new assign_header(
9460 $instance,
9461 $this->get_context(),
9462 $this->show_intro(),
9463 $this->get_course_module()->id
9467 $confirm = optional_param('confirm', 0, PARAM_BOOL);
9469 if ($confirm) {
9470 if (confirm_sesskey()) {
9471 // Fix the grades.
9472 $this->fix_null_grades();
9473 unset_config('has_rescaled_null_grades_' . $instance->id, 'assign');
9474 // Display the success notice.
9475 $o .= $this->get_renderer()->notification(get_string('fixrescalednullgradesdone', 'assign'), 'notifysuccess');
9476 } else {
9477 // If the sesskey is not valid, then display the error notice.
9478 $o .= $this->get_renderer()->notification(get_string('invalidsesskey', 'error'), 'notifyerror');
9480 $url = new moodle_url(
9481 url: '/mod/assign/view.php',
9482 params: [
9483 'id' => $this->get_course_module()->id,
9484 'action' => 'grading',
9487 $o .= $this->get_renderer()->continue_button($url);
9488 } else {
9489 // Ask for confirmation.
9490 $continue = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades', 'confirm' => true, 'sesskey' => sesskey()));
9491 $cancel = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
9492 $o .= $OUTPUT->confirm(get_string('fixrescalednullgradesconfirm', 'mod_assign'), $continue, $cancel);
9495 $o .= $this->view_footer();
9497 return $o;
9501 * Set the most recent submission for the team.
9502 * The most recent team submission is used to determine if another attempt should be created when allowing another
9503 * attempt on a group assignment, and whether the gradebook should be updated.
9505 * @since Moodle 3.4
9506 * @param stdClass $submission The most recent submission of the group.
9508 public function set_most_recent_team_submission($submission) {
9509 $this->mostrecentteamsubmission = $submission;
9513 * Return array of valid grading allocation filters for the grading interface.
9515 * @param boolean $export Export the list of filters for a template.
9516 * @return array
9518 public function get_marking_allocation_filters($export = false) {
9519 $markingallocation = $this->get_instance()->markingworkflow &&
9520 $this->get_instance()->markingallocation &&
9521 has_capability('mod/assign:manageallocations', $this->context);
9522 // Get markers to use in drop lists.
9523 $markingallocationoptions = array();
9524 if ($markingallocation) {
9525 list($sort, $params) = users_order_by_sql('u');
9526 // Only enrolled users could be assigned as potential markers.
9527 $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
9528 $markingallocationoptions[''] = get_string('filternone', 'assign');
9529 $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
9530 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
9531 foreach ($markers as $marker) {
9532 $markingallocationoptions[$marker->id] = fullname($marker, $viewfullnames);
9535 if ($export) {
9536 $allocationfilter = get_user_preferences('assign_markerfilter', '');
9537 $result = [];
9538 foreach ($markingallocationoptions as $option => $label) {
9539 array_push($result, [
9540 'key' => $option,
9541 'name' => $label,
9542 'active' => ($allocationfilter == $option),
9545 return $result;
9547 return $markingallocationoptions;
9551 * Return array of valid grading workflow filters for the grading interface.
9553 * @param boolean $export Export the list of filters for a template.
9554 * @return array
9556 public function get_marking_workflow_filters($export = false) {
9557 $markingworkflow = $this->get_instance()->markingworkflow;
9558 // Get marking states to show in form.
9559 $markingworkflowoptions = array();
9560 if ($markingworkflow) {
9561 $notmarked = get_string('markingworkflowstatenotmarked', 'assign');
9562 $markingworkflowoptions[''] = get_string('filternone', 'assign');
9563 $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked;
9564 $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user());
9566 if ($export) {
9567 $workflowfilter = get_user_preferences('assign_workflowfilter', '');
9568 $result = [];
9569 foreach ($markingworkflowoptions as $option => $label) {
9570 array_push($result, [
9571 'key' => $option,
9572 'name' => $label,
9573 'active' => ($workflowfilter == $option),
9576 return $result;
9578 return $markingworkflowoptions;
9582 * Return array of valid search filters for the grading interface.
9584 * @param bool $grouped Whether to return the filters grouped or not.
9585 * @return array
9587 public function get_filters(bool $grouped = false): array {
9588 $groupedfilterkeys = [
9590 ASSIGN_FILTER_NOT_SUBMITTED,
9591 ASSIGN_FILTER_DRAFT,
9592 ASSIGN_FILTER_SUBMITTED,
9593 ASSIGN_FILTER_REQUIRE_GRADING,
9596 ASSIGN_FILTER_GRANTED_EXTENSION,
9600 $current = get_user_preferences('assign_filter', '');
9602 $filters = [];
9603 // First is always "no filter" option.
9604 $filters[0] = [
9606 'key' => ASSIGN_FILTER_NONE,
9607 'name' => get_string('filterall', 'assign'),
9608 'active' => ($current == ''),
9612 foreach ($groupedfilterkeys as $group => $filterkeys) {
9613 foreach ($filterkeys as $key) {
9614 $filters[$group] = $filters[$group] ?? [];
9615 $filters[$group][] = [
9616 'key' => $key,
9617 'name' => get_string('filter' . $key, 'assign'),
9618 'active' => ($current == $key),
9623 return $grouped ? $filters : array_merge(...$filters);
9627 * Get the correct submission statement depending on single submisison, team submission or team submission
9628 * where all team memebers must submit.
9630 * @param stdClass $adminconfig
9631 * @param stdClass $instance
9632 * @param context $context
9634 * @return string
9636 protected function get_submissionstatement($adminconfig, $instance, $context) {
9637 $submissionstatement = '';
9639 if (!($context instanceof context)) {
9640 return $submissionstatement;
9643 // Single submission.
9644 if (!$instance->teamsubmission) {
9645 // Single submission statement is not empty.
9646 if (!empty($adminconfig->submissionstatement)) {
9647 // Format the submission statement before its sent. We turn off para because this is going within
9648 // a form element.
9649 $options = array(
9650 'context' => $context,
9651 'para' => false
9653 $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
9655 } else { // Team submission.
9656 // One user can submit for the whole team.
9657 if (!empty($adminconfig->submissionstatementteamsubmission) && !$instance->requireallteammemberssubmit) {
9658 // Format the submission statement before its sent. We turn off para because this is going within
9659 // a form element.
9660 $options = array(
9661 'context' => $context,
9662 'para' => false
9664 $submissionstatement = format_text($adminconfig->submissionstatementteamsubmission,
9665 FORMAT_MOODLE, $options);
9666 } else if (!empty($adminconfig->submissionstatementteamsubmissionallsubmit) &&
9667 $instance->requireallteammemberssubmit) {
9668 // All team members must submit.
9669 // Format the submission statement before its sent. We turn off para because this is going within
9670 // a form element.
9671 $options = array(
9672 'context' => $context,
9673 'para' => false
9675 $submissionstatement = format_text($adminconfig->submissionstatementteamsubmissionallsubmit,
9676 FORMAT_MOODLE, $options);
9680 return $submissionstatement;
9684 * Check if time limit for assignment enabled and set up.
9686 * @param int|null $userid User ID. If null, use global user.
9687 * @return bool
9689 public function is_time_limit_enabled(?int $userid = null): bool {
9690 $instance = $this->get_instance($userid);
9691 return get_config('assign', 'enabletimelimit') && !empty($instance->timelimit);
9695 * Check if an assignment submission is already started and not yet submitted.
9697 * @param int|null $userid User ID. If null, use global user.
9698 * @param int $groupid Group ID. If 0, use user id to determine group.
9699 * @param int $attemptnumber Attempt number. If -1, check latest submission.
9700 * @return bool
9702 public function is_attempt_in_progress(?int $userid = null, int $groupid = 0, int $attemptnumber = -1): bool {
9703 if ($this->get_instance($userid)->teamsubmission) {
9704 $submission = $this->get_group_submission($userid, $groupid, false, $attemptnumber);
9705 } else {
9706 $submission = $this->get_user_submission($userid, false, $attemptnumber);
9709 // If time limit is enabled, we only assume it is in progress if there is a start time for submission.
9710 $timedattemptstarted = true;
9711 if ($this->is_time_limit_enabled($userid)) {
9712 $timedattemptstarted = !empty($submission) && !empty($submission->timestarted);
9715 return !empty($submission) && $submission->status !== ASSIGN_SUBMISSION_STATUS_SUBMITTED && $timedattemptstarted;
9720 * Portfolio caller class for mod_assign.
9722 * @package mod_assign
9723 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
9724 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
9726 class assign_portfolio_caller extends portfolio_module_caller_base {
9728 /** @var int callback arg - the id of submission we export */
9729 protected $sid;
9731 /** @var string component of the submission files we export*/
9732 protected $component;
9734 /** @var string callback arg - the area of submission files we export */
9735 protected $area;
9737 /** @var int callback arg - the id of file we export */
9738 protected $fileid;
9740 /** @var int callback arg - the cmid of the assignment we export */
9741 protected $cmid;
9743 /** @var string callback arg - the plugintype of the editor we export */
9744 protected $plugin;
9746 /** @var string callback arg - the name of the editor field we export */
9747 protected $editor;
9750 * Callback arg for a single file export.
9752 public static function expected_callbackargs() {
9753 return array(
9754 'cmid' => true,
9755 'sid' => false,
9756 'area' => false,
9757 'component' => false,
9758 'fileid' => false,
9759 'plugin' => false,
9760 'editor' => false,
9765 * The constructor.
9767 * @param array $callbackargs
9769 public function __construct($callbackargs) {
9770 parent::__construct($callbackargs);
9771 $this->cm = get_coursemodule_from_id('assign', $this->cmid, 0, false, MUST_EXIST);
9775 * Load data needed for the portfolio export.
9777 * If the assignment type implements portfolio_load_data(), the processing is delegated
9778 * to it. Otherwise, the caller must provide either fileid (to export single file) or
9779 * submissionid and filearea (to export all data attached to the given submission file area)
9780 * via callback arguments.
9782 * @throws portfolio_caller_exception
9784 public function load_data() {
9785 global $DB;
9787 $context = context_module::instance($this->cmid);
9789 if (empty($this->fileid)) {
9790 if (empty($this->sid) || empty($this->area)) {
9791 throw new portfolio_caller_exception('invalidfileandsubmissionid', 'mod_assign');
9794 $submission = $DB->get_record('assign_submission', array('id' => $this->sid));
9795 } else {
9796 $submissionid = $DB->get_field('files', 'itemid', array('id' => $this->fileid, 'contextid' => $context->id));
9797 if ($submissionid) {
9798 $submission = $DB->get_record('assign_submission', array('id' => $submissionid));
9802 if (empty($submission)) {
9803 throw new portfolio_caller_exception('filenotfound');
9804 } else if ($submission->userid == 0) {
9805 // This must be a group submission.
9806 if (!groups_is_member($submission->groupid, $this->user->id)) {
9807 throw new portfolio_caller_exception('filenotfound');
9809 } else if ($this->user->id != $submission->userid) {
9810 throw new portfolio_caller_exception('filenotfound');
9813 // Export either an area of files or a single file (see function for more detail).
9814 // The first arg is an id or null. If it is an id, the rest of the args are ignored.
9815 // If it is null, the rest of the args are used to load a list of files from get_areafiles.
9816 $this->set_file_and_format_data($this->fileid,
9817 $context->id,
9818 $this->component,
9819 $this->area,
9820 $this->sid,
9821 'timemodified',
9822 false);
9827 * Prepares the package up before control is passed to the portfolio plugin.
9829 * @throws portfolio_caller_exception
9830 * @return mixed
9832 public function prepare_package() {
9834 if ($this->plugin && $this->editor) {
9835 $options = portfolio_format_text_options();
9836 $context = context_module::instance($this->cmid);
9837 $options->context = $context;
9839 $plugin = $this->get_submission_plugin();
9841 $text = $plugin->get_editor_text($this->editor, $this->sid);
9842 $format = $plugin->get_editor_format($this->editor, $this->sid);
9844 $html = format_text($text, $format, $options);
9845 $html = portfolio_rewrite_pluginfile_urls($html,
9846 $context->id,
9847 'mod_assign',
9848 $this->area,
9849 $this->sid,
9850 $this->exporter->get('format'));
9852 $exporterclass = $this->exporter->get('formatclass');
9853 if (in_array($exporterclass, array(PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_RICHHTML))) {
9854 if ($files = $this->exporter->get('caller')->get('multifiles')) {
9855 foreach ($files as $file) {
9856 $this->exporter->copy_existing_file($file);
9859 return $this->exporter->write_new_file($html, 'assignment.html', !empty($files));
9860 } else if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
9861 $leapwriter = $this->exporter->get('format')->leap2a_writer();
9862 $entry = new portfolio_format_leap2a_entry($this->area . $this->cmid,
9863 $context->get_context_name(),
9864 'resource',
9865 $html);
9867 $entry->add_category('web', 'resource_type');
9868 $entry->author = $this->user;
9869 $leapwriter->add_entry($entry);
9870 if ($files = $this->exporter->get('caller')->get('multifiles')) {
9871 $leapwriter->link_files($entry, $files, $this->area . $this->cmid . 'file');
9872 foreach ($files as $file) {
9873 $this->exporter->copy_existing_file($file);
9876 return $this->exporter->write_new_file($leapwriter->to_xml(),
9877 $this->exporter->get('format')->manifest_name(),
9878 true);
9879 } else {
9880 debugging('invalid format class: ' . $this->exporter->get('formatclass'));
9885 if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
9886 $leapwriter = $this->exporter->get('format')->leap2a_writer();
9887 $files = array();
9888 if ($this->singlefile) {
9889 $files[] = $this->singlefile;
9890 } else if ($this->multifiles) {
9891 $files = $this->multifiles;
9892 } else {
9893 throw new portfolio_caller_exception('invalidpreparepackagefile',
9894 'portfolio',
9895 $this->get_return_url());
9898 $entryids = array();
9899 foreach ($files as $file) {
9900 $entry = new portfolio_format_leap2a_file($file->get_filename(), $file);
9901 $entry->author = $this->user;
9902 $leapwriter->add_entry($entry);
9903 $this->exporter->copy_existing_file($file);
9904 $entryids[] = $entry->id;
9906 if (count($files) > 1) {
9907 $baseid = 'assign' . $this->cmid . $this->area;
9908 $context = context_module::instance($this->cmid);
9910 // If we have multiple files, they should be grouped together into a folder.
9911 $entry = new portfolio_format_leap2a_entry($baseid . 'group',
9912 $context->get_context_name(),
9913 'selection');
9914 $leapwriter->add_entry($entry);
9915 $leapwriter->make_selection($entry, $entryids, 'Folder');
9917 return $this->exporter->write_new_file($leapwriter->to_xml(),
9918 $this->exporter->get('format')->manifest_name(),
9919 true);
9921 return $this->prepare_package_file();
9925 * Fetch the plugin by its type.
9927 * @return assign_submission_plugin
9929 protected function get_submission_plugin() {
9930 global $CFG;
9931 if (!$this->plugin || !$this->cmid) {
9932 return null;
9935 require_once($CFG->dirroot . '/mod/assign/locallib.php');
9937 $context = context_module::instance($this->cmid);
9939 $assignment = new assign($context, null, null);
9940 return $assignment->get_submission_plugin_by_type($this->plugin);
9944 * Calculate a sha1 has of either a single file or a list
9945 * of files based on the data set by load_data.
9947 * @return string
9949 public function get_sha1() {
9951 if ($this->plugin && $this->editor) {
9952 $plugin = $this->get_submission_plugin();
9953 $options = portfolio_format_text_options();
9954 $options->context = context_module::instance($this->cmid);
9956 $text = format_text($plugin->get_editor_text($this->editor, $this->sid),
9957 $plugin->get_editor_format($this->editor, $this->sid),
9958 $options);
9959 $textsha1 = sha1($text);
9960 $filesha1 = '';
9961 try {
9962 $filesha1 = $this->get_sha1_file();
9963 } catch (portfolio_caller_exception $e) {
9964 // No files.
9966 return sha1($textsha1 . $filesha1);
9968 return $this->get_sha1_file();
9972 * Calculate the time to transfer either a single file or a list
9973 * of files based on the data set by load_data.
9975 * @return int
9977 public function expected_time() {
9978 return $this->expected_time_file();
9982 * Checking the permissions.
9984 * @return bool
9986 public function check_permissions() {
9987 $context = context_module::instance($this->cmid);
9988 return has_capability('mod/assign:exportownsubmission', $context);
9992 * Display a module name.
9994 * @return string
9996 public static function display_name() {
9997 return get_string('modulename', 'assign');
10001 * Return array of formats supported by this portfolio call back.
10003 * @return array
10005 public static function base_supported_formats() {
10006 return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_LEAP2A);
10011 * Logic to happen when a/some group(s) has/have been deleted in a course.
10013 * @param int $courseid The course ID.
10014 * @param int $groupid The group id if it is known
10015 * @return void
10017 function assign_process_group_deleted_in_course($courseid, $groupid = null) {
10018 global $DB;
10020 $params = array('courseid' => $courseid);
10021 if ($groupid) {
10022 $params['groupid'] = $groupid;
10023 // We just update the group that was deleted.
10024 $sql = "SELECT o.id, o.assignid, o.groupid
10025 FROM {assign_overrides} o
10026 JOIN {assign} assign ON assign.id = o.assignid
10027 WHERE assign.course = :courseid
10028 AND o.groupid = :groupid";
10029 } else {
10030 // No groupid, we update all orphaned group overrides for all assign in course.
10031 $sql = "SELECT o.id, o.assignid, o.groupid
10032 FROM {assign_overrides} o
10033 JOIN {assign} assign ON assign.id = o.assignid
10034 LEFT JOIN {groups} grp ON grp.id = o.groupid
10035 WHERE assign.course = :courseid
10036 AND o.groupid IS NOT NULL
10037 AND grp.id IS NULL";
10039 $records = $DB->get_records_sql($sql, $params);
10040 if (!$records) {
10041 return; // Nothing to do.
10043 $DB->delete_records_list('assign_overrides', 'id', array_keys($records));
10044 $cache = cache::make('mod_assign', 'overrides');
10045 foreach ($records as $record) {
10046 $cache->delete("{$record->assignid}_g_{$record->groupid}");
10051 * Change the sort order of an override
10053 * @param int $id of the override
10054 * @param string $move direction of move
10055 * @param int $assignid of the assignment
10056 * @return bool success of operation
10058 function move_group_override($id, $move, $assignid) {
10059 global $DB;
10061 // Get the override object.
10062 if (!$override = $DB->get_record('assign_overrides', ['id' => $id, 'assignid' => $assignid], 'id, sortorder, groupid')) {
10063 return false;
10065 // Count the number of group overrides.
10066 $overridecountgroup = $DB->count_records('assign_overrides', array('userid' => null, 'assignid' => $assignid));
10068 // Calculate the new sortorder.
10069 if ( ($move == 'up') and ($override->sortorder > 1)) {
10070 $neworder = $override->sortorder - 1;
10071 } else if (($move == 'down') and ($override->sortorder < $overridecountgroup)) {
10072 $neworder = $override->sortorder + 1;
10073 } else {
10074 return false;
10077 // Retrieve the override object that is currently residing in the new position.
10078 $params = ['sortorder' => $neworder, 'assignid' => $assignid];
10079 if ($swapoverride = $DB->get_record('assign_overrides', $params, 'id, sortorder, groupid')) {
10081 // Swap the sortorders.
10082 $swapoverride->sortorder = $override->sortorder;
10083 $override->sortorder = $neworder;
10085 // Update the override records.
10086 $DB->update_record('assign_overrides', $override);
10087 $DB->update_record('assign_overrides', $swapoverride);
10089 // Delete cache for the 2 records we updated above.
10090 $cache = cache::make('mod_assign', 'overrides');
10091 $cache->delete("{$assignid}_g_{$override->groupid}");
10092 $cache->delete("{$assignid}_g_{$swapoverride->groupid}");
10095 reorder_group_overrides($assignid);
10096 return true;
10100 * Reorder the overrides starting at the override at the given startorder.
10102 * @param int $assignid of the assigment
10104 function reorder_group_overrides($assignid) {
10105 global $DB;
10107 $i = 1;
10108 if ($overrides = $DB->get_records('assign_overrides', array('userid' => null, 'assignid' => $assignid), 'sortorder ASC')) {
10109 $cache = cache::make('mod_assign', 'overrides');
10110 foreach ($overrides as $override) {
10111 $f = new stdClass();
10112 $f->id = $override->id;
10113 $f->sortorder = $i++;
10114 $DB->update_record('assign_overrides', $f);
10115 $cache->delete("{$assignid}_g_{$override->groupid}");
10117 // Update priorities of group overrides.
10118 $params = [
10119 'modulename' => 'assign',
10120 'instance' => $override->assignid,
10121 'groupid' => $override->groupid
10123 $DB->set_field('event', 'priority', $f->sortorder, $params);
10129 * Get the information about the standard assign JavaScript module.
10130 * @return array a standard jsmodule structure.
10132 function assign_get_js_module() {
10133 return array(
10134 'name' => 'mod_assign',
10135 'fullpath' => '/mod/assign/module.js',