Merge branch 'MDL-77343-master' of https://github.com/meirzamoodle/moodle
[moodle.git] / mod / assign / locallib.php
blob656e85e73e3d91971583e3fc9a0c3ca2d4255234
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * This file contains the definition for the class assignment
20 * This class provides all the functionality for the new assign module.
22 * @package mod_assign
23 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
29 // Assignment submission statuses.
30 define('ASSIGN_SUBMISSION_STATUS_NEW', 'new');
31 define('ASSIGN_SUBMISSION_STATUS_REOPENED', 'reopened');
32 define('ASSIGN_SUBMISSION_STATUS_DRAFT', 'draft');
33 define('ASSIGN_SUBMISSION_STATUS_SUBMITTED', 'submitted');
35 // Search filters for grading page.
36 define('ASSIGN_FILTER_NONE', 'none');
37 define('ASSIGN_FILTER_SUBMITTED', 'submitted');
38 define('ASSIGN_FILTER_NOT_SUBMITTED', 'notsubmitted');
39 define('ASSIGN_FILTER_SINGLE_USER', 'singleuser');
40 define('ASSIGN_FILTER_REQUIRE_GRADING', 'requiregrading');
41 define('ASSIGN_FILTER_GRANTED_EXTENSION', 'grantedextension');
42 define('ASSIGN_FILTER_DRAFT', 'draft');
44 // Marker filter for grading page.
45 define('ASSIGN_MARKER_FILTER_NO_MARKER', -1);
47 // Reopen attempt methods.
48 define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none');
49 define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual');
50 define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass');
52 // Special value means allow unlimited attempts.
53 define('ASSIGN_UNLIMITED_ATTEMPTS', -1);
55 // Special value means no grade has been set.
56 define('ASSIGN_GRADE_NOT_SET', -1);
58 // Grading states.
59 define('ASSIGN_GRADING_STATUS_GRADED', 'graded');
60 define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded');
62 // Marking workflow states.
63 define('ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED', 'notmarked');
64 define('ASSIGN_MARKING_WORKFLOW_STATE_INMARKING', 'inmarking');
65 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW', 'readyforreview');
66 define('ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW', 'inreview');
67 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE', 'readyforrelease');
68 define('ASSIGN_MARKING_WORKFLOW_STATE_RELEASED', 'released');
70 /** ASSIGN_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
71 define("ASSIGN_MAX_EVENT_LENGTH", "432000");
73 // Name of file area for intro attachments.
74 define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment');
76 // Name of file area for activity attachments.
77 define('ASSIGN_ACTIVITYATTACHMENT_FILEAREA', 'activityattachment');
79 // Event types.
80 define('ASSIGN_EVENT_TYPE_DUE', 'due');
81 define('ASSIGN_EVENT_TYPE_GRADINGDUE', 'gradingdue');
82 define('ASSIGN_EVENT_TYPE_OPEN', 'open');
83 define('ASSIGN_EVENT_TYPE_CLOSE', 'close');
85 require_once($CFG->libdir . '/accesslib.php');
86 require_once($CFG->libdir . '/formslib.php');
87 require_once($CFG->dirroot . '/repository/lib.php');
88 require_once($CFG->dirroot . '/mod/assign/mod_form.php');
89 require_once($CFG->libdir . '/gradelib.php');
90 require_once($CFG->dirroot . '/grade/grading/lib.php');
91 require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php');
92 require_once($CFG->dirroot . '/mod/assign/submissionplugin.php');
93 require_once($CFG->dirroot . '/mod/assign/renderable.php');
94 require_once($CFG->dirroot . '/mod/assign/gradingtable.php');
95 require_once($CFG->libdir . '/portfolio/caller.php');
97 use mod_assign\event\submission_removed;
98 use mod_assign\event\submission_status_updated;
99 use \mod_assign\output\grading_app;
100 use \mod_assign\output\assign_header;
101 use \mod_assign\output\assign_submission_status;
102 use mod_assign\output\timelimit_panel;
103 use mod_assign\downloader;
106 * Standard base class for mod_assign (assignment types).
108 * @package mod_assign
109 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
110 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
112 class assign {
114 /** @var stdClass the assignment record that contains the global settings for this assign instance */
115 private $instance;
117 /** @var array $var array an array containing per-user assignment records, each having calculated properties (e.g. dates) */
118 private $userinstances = [];
120 /** @var grade_item the grade_item record for this assign instance's primary grade item. */
121 private $gradeitem;
123 /** @var context the context of the course module for this assign instance
124 * (or just the course if we are creating a new one)
126 private $context;
128 /** @var stdClass the course this assign instance belongs to */
129 private $course;
131 /** @var stdClass the admin config for all assign instances */
132 private $adminconfig;
134 /** @var assign_renderer the custom renderer for this module */
135 private $output;
137 /** @var cm_info the course module for this assign instance */
138 private $coursemodule;
140 /** @var array cache for things like the coursemodule name or the scale menu -
141 * only lives for a single request.
143 private $cache;
145 /** @var array list of the installed submission plugins */
146 private $submissionplugins;
148 /** @var array list of the installed feedback plugins */
149 private $feedbackplugins;
151 /** @var string action to be used to return to this page
152 * (without repeating any form submissions etc).
154 private $returnaction = 'view';
156 /** @var array params to be used to return to this page */
157 private $returnparams = array();
159 /** @var string modulename prevents excessive calls to get_string */
160 private static $modulename = null;
162 /** @var string modulenameplural prevents excessive calls to get_string */
163 private static $modulenameplural = null;
165 /** @var array of marking workflow states for the current user */
166 private $markingworkflowstates = null;
168 /** @var bool whether to exclude users with inactive enrolment */
169 private $showonlyactiveenrol = null;
171 /** @var string A key used to identify userlists created by this object. */
172 private $useridlistid = null;
174 /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */
175 private $participants = array();
177 /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */
178 private $usersubmissiongroups = array();
180 /** @var array cached list of user groups. The cache key will be the user. */
181 private $usergroups = array();
183 /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */
184 private $sharedgroupmembers = array();
187 * @var stdClass The most recent team submission. Used to determine additional attempt numbers and whether
188 * to update the gradebook.
190 private $mostrecentteamsubmission = null;
192 /** @var array Array of error messages encountered during the execution of assignment related operations. */
193 private $errors = array();
195 /** @var mixed This var can vary between false for no overrides to a stdClass of the overrides for a group */
196 private $overridedata;
198 /** @var float grade value. */
199 public $grade;
202 * Constructor for the base assign class.
204 * Note: For $coursemodule you can supply a stdclass if you like, but it
205 * will be more efficient to supply a cm_info object.
207 * @param mixed $coursemodulecontext context|null the course module context
208 * (or the course context if the coursemodule has not been
209 * created yet).
210 * @param mixed $coursemodule the current course module if it was already loaded,
211 * otherwise this class will load one from the context as required.
212 * @param mixed $course the current course if it was already loaded,
213 * otherwise this class will load one from the context as required.
215 public function __construct($coursemodulecontext, $coursemodule, $course) {
216 $this->context = $coursemodulecontext;
217 $this->course = $course;
219 // Ensure that $this->coursemodule is a cm_info object (or null).
220 $this->coursemodule = cm_info::create($coursemodule);
222 // Temporary cache only lives for a single request - used to reduce db lookups.
223 $this->cache = array();
225 $this->submissionplugins = $this->load_plugins('assignsubmission');
226 $this->feedbackplugins = $this->load_plugins('assignfeedback');
228 // Extra entropy is required for uniqid() to work on cygwin.
229 $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM);
233 * Set the action and parameters that can be used to return to the current page.
235 * @param string $action The action for the current page
236 * @param array $params An array of name value pairs which form the parameters
237 * to return to the current page.
238 * @return void
240 public function register_return_link($action, $params) {
241 global $PAGE;
242 $params['action'] = $action;
243 $cm = $this->get_course_module();
244 if ($cm) {
245 $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $cm->id));
246 } else {
247 $currenturl = new moodle_url('/mod/assign/index.php', array('id' => $this->get_course()->id));
250 $currenturl->params($params);
251 $PAGE->set_url($currenturl);
255 * Return an action that can be used to get back to the current page.
257 * @return string action
259 public function get_return_action() {
260 global $PAGE;
262 // Web services don't set a URL, we should avoid debugging when ussing the url object.
263 if (!WS_SERVER) {
264 $params = $PAGE->url->params();
267 if (!empty($params['action'])) {
268 return $params['action'];
270 return '';
274 * Based on the current assignment settings should we display the intro.
276 * @return bool showintro
278 public function show_intro() {
279 if ($this->get_instance()->alwaysshowdescription ||
280 time() > $this->get_instance()->allowsubmissionsfromdate) {
281 return true;
283 return false;
287 * Return a list of parameters that can be used to get back to the current page.
289 * @return array params
291 public function get_return_params() {
292 global $PAGE;
294 $params = array();
295 if (!WS_SERVER) {
296 $params = $PAGE->url->params();
298 unset($params['id']);
299 unset($params['action']);
300 return $params;
304 * Set the submitted form data.
306 * @param stdClass $data The form data (instance)
308 public function set_instance(stdClass $data) {
309 $this->instance = $data;
313 * Set the context.
315 * @param context $context The new context
317 public function set_context(context $context) {
318 $this->context = $context;
322 * Set the course data.
324 * @param stdClass $course The course data
326 public function set_course(stdClass $course) {
327 $this->course = $course;
331 * Set error message.
333 * @param string $message The error message
335 protected function set_error_message(string $message) {
336 $this->errors[] = $message;
340 * Get error messages.
342 * @return array The array of error messages
344 protected function get_error_messages(): array {
345 return $this->errors;
349 * Get list of feedback plugins installed.
351 * @return array
353 public function get_feedback_plugins() {
354 return $this->feedbackplugins;
358 * Get list of submission plugins installed.
360 * @return array
362 public function get_submission_plugins() {
363 return $this->submissionplugins;
367 * Is blind marking enabled and reveal identities not set yet?
369 * @return bool
371 public function is_blind_marking() {
372 return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities;
376 * Is hidden grading enabled?
378 * This just checks the assignment settings. Remember to check
379 * the user has the 'showhiddengrader' capability too
381 * @return bool
383 public function is_hidden_grader() {
384 return $this->get_instance()->hidegrader;
388 * Does an assignment have submission(s) or grade(s) already?
390 * @return bool
392 public function has_submissions_or_grades() {
393 $allgrades = $this->count_grades();
394 $allsubmissions = $this->count_submissions();
395 if (($allgrades == 0) && ($allsubmissions == 0)) {
396 return false;
398 return true;
402 * Get a specific submission plugin by its type.
404 * @param string $subtype assignsubmission | assignfeedback
405 * @param string $type
406 * @return mixed assign_plugin|null
408 public function get_plugin_by_type($subtype, $type) {
409 $shortsubtype = substr($subtype, strlen('assign'));
410 $name = $shortsubtype . 'plugins';
411 if ($name != 'feedbackplugins' && $name != 'submissionplugins') {
412 return null;
414 $pluginlist = $this->$name;
415 foreach ($pluginlist as $plugin) {
416 if ($plugin->get_type() == $type) {
417 return $plugin;
420 return null;
424 * Get a feedback plugin by type.
426 * @param string $type - The type of plugin e.g comments
427 * @return mixed assign_feedback_plugin|null
429 public function get_feedback_plugin_by_type($type) {
430 return $this->get_plugin_by_type('assignfeedback', $type);
434 * Get a submission plugin by type.
436 * @param string $type - The type of plugin e.g comments
437 * @return mixed assign_submission_plugin|null
439 public function get_submission_plugin_by_type($type) {
440 return $this->get_plugin_by_type('assignsubmission', $type);
444 * Load the plugins from the sub folders under subtype.
446 * @param string $subtype - either submission or feedback
447 * @return array - The sorted list of plugins
449 public function load_plugins($subtype) {
450 global $CFG;
451 $result = array();
453 $names = core_component::get_plugin_list($subtype);
455 foreach ($names as $name => $path) {
456 if (file_exists($path . '/locallib.php')) {
457 require_once($path . '/locallib.php');
459 $shortsubtype = substr($subtype, strlen('assign'));
460 $pluginclass = 'assign_' . $shortsubtype . '_' . $name;
462 $plugin = new $pluginclass($this, $name);
464 if ($plugin instanceof assign_plugin) {
465 $idx = $plugin->get_sort_order();
466 while (array_key_exists($idx, $result)) {
467 $idx +=1;
469 $result[$idx] = $plugin;
473 ksort($result);
474 return $result;
478 * Display the assignment, used by view.php
480 * The assignment is displayed differently depending on your role,
481 * the settings for the assignment and the status of the assignment.
483 * @param string $action The current action if any.
484 * @param array $args Optional arguments to pass to the view (instead of getting them from GET and POST).
485 * @return string - The page output.
487 public function view($action='', $args = array()) {
488 global $PAGE;
490 $o = '';
491 $mform = null;
492 $notices = array();
493 $nextpageparams = array();
495 if (!empty($this->get_course_module()->id)) {
496 $nextpageparams['id'] = $this->get_course_module()->id;
499 if (empty($action)) {
500 $PAGE->add_body_class('limitedwidth');
503 // Handle form submissions first.
504 if ($action == 'savesubmission') {
505 $action = 'editsubmission';
506 if ($this->process_save_submission($mform, $notices)) {
507 $action = 'redirect';
508 if ($this->can_grade()) {
509 $nextpageparams['action'] = 'grading';
510 } else {
511 $nextpageparams['action'] = 'view';
514 } else if ($action == 'editprevioussubmission') {
515 $action = 'editsubmission';
516 if ($this->process_copy_previous_attempt($notices)) {
517 $action = 'redirect';
518 $nextpageparams['action'] = 'editsubmission';
520 } else if ($action == 'lock') {
521 $this->process_lock_submission();
522 $action = 'redirect';
523 $nextpageparams['action'] = 'grading';
524 } else if ($action == 'removesubmission') {
525 $this->process_remove_submission();
526 $action = 'redirect';
527 if ($this->can_grade()) {
528 $nextpageparams['action'] = 'grading';
529 } else {
530 $nextpageparams['action'] = 'view';
532 } else if ($action == 'addattempt') {
533 $this->process_add_attempt(required_param('userid', PARAM_INT));
534 $action = 'redirect';
535 $nextpageparams['action'] = 'grading';
536 } else if ($action == 'reverttodraft') {
537 $this->process_revert_to_draft();
538 $action = 'redirect';
539 $nextpageparams['action'] = 'grading';
540 } else if ($action == 'unlock') {
541 $this->process_unlock_submission();
542 $action = 'redirect';
543 $nextpageparams['action'] = 'grading';
544 } else if ($action == 'setbatchmarkingworkflowstate') {
545 $this->process_set_batch_marking_workflow_state();
546 $action = 'redirect';
547 $nextpageparams['action'] = 'grading';
548 } else if ($action == 'setbatchmarkingallocation') {
549 $this->process_set_batch_marking_allocation();
550 $action = 'redirect';
551 $nextpageparams['action'] = 'grading';
552 } else if ($action == 'confirmsubmit') {
553 $action = 'submit';
554 if ($this->process_submit_for_grading($mform, $notices)) {
555 $action = 'redirect';
556 $nextpageparams['action'] = 'view';
557 } else if ($notices) {
558 $action = 'viewsubmitforgradingerror';
560 } else if ($action == 'submitotherforgrading') {
561 if ($this->process_submit_other_for_grading($mform, $notices)) {
562 $action = 'redirect';
563 $nextpageparams['action'] = 'grading';
564 } else {
565 $action = 'viewsubmitforgradingerror';
567 } else if ($action == 'gradingbatchoperation') {
568 $action = $this->process_grading_batch_operation($mform);
569 if ($action == 'grading') {
570 $action = 'redirect';
571 $nextpageparams['action'] = 'grading';
573 } else if ($action == 'submitgrade') {
574 if (optional_param('saveandshownext', null, PARAM_RAW)) {
575 // Save and show next.
576 $action = 'grade';
577 if ($this->process_save_grade($mform)) {
578 $action = 'redirect';
579 $nextpageparams['action'] = 'grade';
580 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
581 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
583 } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) {
584 $action = 'redirect';
585 $nextpageparams['action'] = 'grade';
586 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1;
587 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
588 } else if (optional_param('nosaveandnext', null, PARAM_RAW)) {
589 $action = 'redirect';
590 $nextpageparams['action'] = 'grade';
591 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1;
592 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
593 } else if (optional_param('savegrade', null, PARAM_RAW)) {
594 // Save changes button.
595 $action = 'grade';
596 if ($this->process_save_grade($mform)) {
597 $action = 'redirect';
598 $nextpageparams['action'] = 'savegradingresult';
600 } else {
601 // Cancel button.
602 $action = 'redirect';
603 $nextpageparams['action'] = 'grading';
605 } else if ($action == 'quickgrade') {
606 $message = $this->process_save_quick_grades();
607 $action = 'quickgradingresult';
608 } else if ($action == 'saveoptions') {
609 $this->process_save_grading_options();
610 $action = 'redirect';
611 $nextpageparams['action'] = 'grading';
612 } else if ($action == 'saveextension') {
613 $action = 'grantextension';
614 if ($this->process_save_extension($mform)) {
615 $action = 'redirect';
616 $nextpageparams['action'] = 'grading';
618 } else if ($action == 'revealidentitiesconfirm') {
619 $this->process_reveal_identities();
620 $action = 'redirect';
621 $nextpageparams['action'] = 'grading';
624 $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT),
625 'useridlistid' => optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM));
626 $this->register_return_link($action, $returnparams);
628 // Include any page action as part of the body tag CSS id.
629 if (!empty($action)) {
630 $PAGE->set_pagetype('mod-assign-' . $action);
632 // Now show the right view page.
633 if ($action == 'redirect') {
634 $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams);
635 $messages = '';
636 $messagetype = \core\output\notification::NOTIFY_INFO;
637 $errors = $this->get_error_messages();
638 if (!empty($errors)) {
639 $messages = html_writer::alist($errors, ['class' => 'mb-1 mt-1']);
640 $messagetype = \core\output\notification::NOTIFY_ERROR;
642 redirect($nextpageurl, $messages, null, $messagetype);
643 return;
644 } else if ($action == 'savegradingresult') {
645 $message = get_string('gradingchangessaved', 'assign');
646 $o .= $this->view_savegrading_result($message);
647 } else if ($action == 'quickgradingresult') {
648 $mform = null;
649 $o .= $this->view_quickgrading_result($message);
650 } else if ($action == 'gradingpanel') {
651 $o .= $this->view_single_grading_panel($args);
652 } else if ($action == 'grade') {
653 $o .= $this->view_single_grade_page($mform);
654 } else if ($action == 'viewpluginassignfeedback') {
655 $o .= $this->view_plugin_content('assignfeedback');
656 } else if ($action == 'viewpluginassignsubmission') {
657 $o .= $this->view_plugin_content('assignsubmission');
658 } else if ($action == 'editsubmission') {
659 $PAGE->add_body_class('limitedwidth');
660 $o .= $this->view_edit_submission_page($mform, $notices);
661 } else if ($action == 'grader') {
662 $o .= $this->view_grader();
663 } else if ($action == 'grading') {
664 $o .= $this->view_grading_page();
665 } else if ($action == 'downloadall') {
666 $o .= $this->download_submissions();
667 } else if ($action == 'submit') {
668 $o .= $this->check_submit_for_grading($mform);
669 } else if ($action == 'grantextension') {
670 $o .= $this->view_grant_extension($mform);
671 } else if ($action == 'revealidentities') {
672 $o .= $this->view_reveal_identities_confirm($mform);
673 } else if ($action == 'removesubmissionconfirm') {
674 $o .= $this->view_remove_submission_confirm();
675 } else if ($action == 'plugingradingbatchoperation') {
676 $o .= $this->view_plugin_grading_batch_operation($mform);
677 } else if ($action == 'viewpluginpage') {
678 $o .= $this->view_plugin_page();
679 } else if ($action == 'viewcourseindex') {
680 $o .= $this->view_course_index();
681 } else if ($action == 'viewbatchsetmarkingworkflowstate') {
682 $o .= $this->view_batch_set_workflow_state($mform);
683 } else if ($action == 'viewbatchmarkingallocation') {
684 $o .= $this->view_batch_markingallocation($mform);
685 } else if ($action == 'viewsubmitforgradingerror') {
686 $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices);
687 } else if ($action == 'fixrescalednullgrades') {
688 $o .= $this->view_fix_rescaled_null_grades();
689 } else {
690 $PAGE->add_body_class('limitedwidth');
691 $o .= $this->view_submission_page();
694 return $o;
698 * Add this instance to the database.
700 * @param stdClass $formdata The data submitted from the form
701 * @param bool $callplugins This is used to skip the plugin code
702 * when upgrading an old assignment to a new one (the plugins get called manually)
703 * @return mixed false if an error occurs or the int id of the new instance
705 public function add_instance(stdClass $formdata, $callplugins) {
706 global $DB;
707 $adminconfig = $this->get_admin_config();
709 $err = '';
711 // Add the database record.
712 $update = new stdClass();
713 $update->name = $formdata->name;
714 $update->timemodified = time();
715 $update->timecreated = time();
716 $update->course = $formdata->course;
717 $update->courseid = $formdata->course;
718 $update->intro = $formdata->intro;
719 $update->introformat = $formdata->introformat;
720 $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
721 if (isset($formdata->activityeditor)) {
722 $update->activity = $this->save_editor_draft_files($formdata);
723 $update->activityformat = $formdata->activityeditor['format'];
725 if (isset($formdata->submissionattachments)) {
726 $update->submissionattachments = $formdata->submissionattachments;
728 $update->submissiondrafts = $formdata->submissiondrafts;
729 $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
730 $update->sendnotifications = $formdata->sendnotifications;
731 $update->sendlatenotifications = $formdata->sendlatenotifications;
732 $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
733 if (isset($formdata->sendstudentnotifications)) {
734 $update->sendstudentnotifications = $formdata->sendstudentnotifications;
736 $update->duedate = $formdata->duedate;
737 $update->cutoffdate = $formdata->cutoffdate;
738 $update->gradingduedate = $formdata->gradingduedate;
739 if (isset($formdata->timelimit)) {
740 $update->timelimit = $formdata->timelimit;
742 $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
743 $update->grade = $formdata->grade;
744 $update->completionsubmit = !empty($formdata->completionsubmit);
745 $update->teamsubmission = $formdata->teamsubmission;
746 $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
747 if (isset($formdata->teamsubmissiongroupingid)) {
748 $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
750 $update->blindmarking = $formdata->blindmarking;
751 if (isset($formdata->hidegrader)) {
752 $update->hidegrader = $formdata->hidegrader;
754 $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
755 if (!empty($formdata->attemptreopenmethod)) {
756 $update->attemptreopenmethod = $formdata->attemptreopenmethod;
758 if (!empty($formdata->maxattempts)) {
759 $update->maxattempts = $formdata->maxattempts;
761 if (isset($formdata->preventsubmissionnotingroup)) {
762 $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
764 $update->markingworkflow = $formdata->markingworkflow;
765 $update->markingallocation = $formdata->markingallocation;
766 if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
767 $update->markingallocation = 0;
770 $returnid = $DB->insert_record('assign', $update);
771 $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
772 // Cache the course record.
773 $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST);
775 $this->save_intro_draft_files($formdata);
776 $this->save_editor_draft_files($formdata);
778 if ($callplugins) {
779 // Call save_settings hook for submission plugins.
780 foreach ($this->submissionplugins as $plugin) {
781 if (!$this->update_plugin_instance($plugin, $formdata)) {
782 throw new \moodle_exception($plugin->get_error());
783 return false;
786 foreach ($this->feedbackplugins as $plugin) {
787 if (!$this->update_plugin_instance($plugin, $formdata)) {
788 throw new \moodle_exception($plugin->get_error());
789 return false;
793 // In the case of upgrades the coursemodule has not been set,
794 // so we need to wait before calling these two.
795 $this->update_calendar($formdata->coursemodule);
796 if (!empty($formdata->completionexpected)) {
797 \core_completion\api::update_completion_date_event($formdata->coursemodule, 'assign', $this->instance,
798 $formdata->completionexpected);
800 $this->update_gradebook(false, $formdata->coursemodule);
804 $update = new stdClass();
805 $update->id = $this->get_instance()->id;
806 $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
807 $DB->update_record('assign', $update);
809 return $returnid;
813 * Delete all grades from the gradebook for this assignment.
815 * @return bool
817 protected function delete_grades() {
818 global $CFG;
820 $result = grade_update('mod/assign',
821 $this->get_course()->id,
822 'mod',
823 'assign',
824 $this->get_instance()->id,
826 null,
827 array('deleted'=>1));
828 return $result == GRADE_UPDATE_OK;
832 * Delete this instance from the database.
834 * @return bool false if an error occurs
836 public function delete_instance() {
837 global $DB;
838 $result = true;
840 foreach ($this->submissionplugins as $plugin) {
841 if (!$plugin->delete_instance()) {
842 throw new \moodle_exception($plugin->get_error());
843 $result = false;
846 foreach ($this->feedbackplugins as $plugin) {
847 if (!$plugin->delete_instance()) {
848 throw new \moodle_exception($plugin->get_error());
849 $result = false;
853 // Delete files associated with this assignment.
854 $fs = get_file_storage();
855 if (! $fs->delete_area_files($this->context->id) ) {
856 $result = false;
859 $this->delete_all_overrides();
861 // Delete_records will throw an exception if it fails - so no need for error checking here.
862 $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id));
863 $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id));
864 $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id));
865 $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id));
866 $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id));
868 // Delete items from the gradebook.
869 if (! $this->delete_grades()) {
870 $result = false;
873 // Delete the instance.
874 // We must delete the module record after we delete the grade item.
875 $DB->delete_records('assign', array('id'=>$this->get_instance()->id));
877 return $result;
881 * Deletes a assign override from the database and clears any corresponding calendar events
883 * @param int $overrideid The id of the override being deleted
884 * @return bool true on success
886 public function delete_override($overrideid) {
887 global $CFG, $DB;
889 require_once($CFG->dirroot . '/calendar/lib.php');
891 $cm = $this->get_course_module();
892 if (empty($cm)) {
893 $instance = $this->get_instance();
894 $cm = get_coursemodule_from_instance('assign', $instance->id, $instance->course);
897 $override = $DB->get_record('assign_overrides', array('id' => $overrideid), '*', MUST_EXIST);
899 // Delete the events.
900 $conds = array('modulename' => 'assign', 'instance' => $this->get_instance()->id);
901 if (isset($override->userid)) {
902 $conds['userid'] = $override->userid;
903 $cachekey = "{$cm->instance}_u_{$override->userid}";
904 } else {
905 $conds['groupid'] = $override->groupid;
906 $cachekey = "{$cm->instance}_g_{$override->groupid}";
908 $events = $DB->get_records('event', $conds);
909 foreach ($events as $event) {
910 $eventold = calendar_event::load($event);
911 $eventold->delete();
914 $DB->delete_records('assign_overrides', array('id' => $overrideid));
915 cache::make('mod_assign', 'overrides')->delete($cachekey);
917 // Set the common parameters for one of the events we will be triggering.
918 $params = array(
919 'objectid' => $override->id,
920 'context' => context_module::instance($cm->id),
921 'other' => array(
922 'assignid' => $override->assignid
925 // Determine which override deleted event to fire.
926 if (!empty($override->userid)) {
927 $params['relateduserid'] = $override->userid;
928 $event = \mod_assign\event\user_override_deleted::create($params);
929 } else {
930 $params['other']['groupid'] = $override->groupid;
931 $event = \mod_assign\event\group_override_deleted::create($params);
934 // Trigger the override deleted event.
935 $event->add_record_snapshot('assign_overrides', $override);
936 $event->trigger();
938 return true;
942 * Deletes all assign overrides from the database and clears any corresponding calendar events
944 public function delete_all_overrides() {
945 global $DB;
947 $overrides = $DB->get_records('assign_overrides', array('assignid' => $this->get_instance()->id), 'id');
948 foreach ($overrides as $override) {
949 $this->delete_override($override->id);
954 * Updates the assign properties with override information for a user.
956 * Algorithm: For each assign setting, if there is a matching user-specific override,
957 * then use that otherwise, if there are group-specific overrides, return the most
958 * lenient combination of them. If neither applies, leave the assign setting unchanged.
960 * @param int $userid The userid.
962 public function update_effective_access($userid) {
964 $override = $this->override_exists($userid);
966 // Merge with assign defaults.
967 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate', 'timelimit');
968 foreach ($keys as $key) {
969 if (isset($override->{$key})) {
970 $this->get_instance($userid)->{$key} = $override->{$key};
977 * Returns whether an assign has any overrides.
979 * @return true if any, false if not
981 public function has_overrides() {
982 global $DB;
984 $override = $DB->record_exists('assign_overrides', array('assignid' => $this->get_instance()->id));
986 if ($override) {
987 return true;
990 return false;
994 * Returns user override
996 * Algorithm: For each assign setting, if there is a matching user-specific override,
997 * then use that otherwise, if there are group-specific overrides, use the one with the
998 * lowest sort order. If neither applies, leave the assign setting unchanged.
1000 * @param int $userid The userid.
1001 * @return stdClass The override
1003 public function override_exists($userid) {
1004 global $DB;
1006 // Gets an assoc array containing the keys for defined user overrides only.
1007 $getuseroverride = function($userid) use ($DB) {
1008 $useroverride = $DB->get_record('assign_overrides', ['assignid' => $this->get_instance()->id, 'userid' => $userid]);
1009 return $useroverride ? get_object_vars($useroverride) : [];
1012 // Gets an assoc array containing the keys for defined group overrides only.
1013 $getgroupoverride = function($userid) use ($DB) {
1014 $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
1016 if (empty($groupings[0])) {
1017 return [];
1020 // Select all overrides that apply to the User's groups.
1021 list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
1022 $sql = "SELECT * FROM {assign_overrides}
1023 WHERE groupid $extra AND assignid = ? ORDER BY sortorder ASC";
1024 $params[] = $this->get_instance()->id;
1025 $groupoverride = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
1027 return $groupoverride ? get_object_vars($groupoverride) : [];
1030 // Later arguments clobber earlier ones with array_merge. The two helper functions
1031 // return arrays containing keys for only the defined overrides. So we get the
1032 // desired behaviour as per the algorithm.
1033 return (object)array_merge(
1034 ['timelimit' => null, 'duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null],
1035 $getgroupoverride($userid),
1036 $getuseroverride($userid)
1041 * Check if the given calendar_event is either a user or group override
1042 * event.
1044 * @return bool
1046 public function is_override_calendar_event(\calendar_event $event) {
1047 global $DB;
1049 if (!isset($event->modulename)) {
1050 return false;
1053 if ($event->modulename != 'assign') {
1054 return false;
1057 if (!isset($event->instance)) {
1058 return false;
1061 if (!isset($event->userid) && !isset($event->groupid)) {
1062 return false;
1065 $overrideparams = [
1066 'assignid' => $event->instance
1069 if (isset($event->groupid)) {
1070 $overrideparams['groupid'] = $event->groupid;
1071 } else if (isset($event->userid)) {
1072 $overrideparams['userid'] = $event->userid;
1075 if ($DB->get_record('assign_overrides', $overrideparams)) {
1076 return true;
1077 } else {
1078 return false;
1083 * This function calculates the minimum and maximum cutoff values for the timestart of
1084 * the given event.
1086 * It will return an array with two values, the first being the minimum cutoff value and
1087 * the second being the maximum cutoff value. Either or both values can be null, which
1088 * indicates there is no minimum or maximum, respectively.
1090 * If a cutoff is required then the function must return an array containing the cutoff
1091 * timestamp and error string to display to the user if the cutoff value is violated.
1093 * A minimum and maximum cutoff return value will look like:
1095 * [1505704373, 'The due date must be after the sbumission start date'],
1096 * [1506741172, 'The due date must be before the cutoff date']
1099 * If the event does not have a valid timestart range then [false, false] will
1100 * be returned.
1102 * @param calendar_event $event The calendar event to get the time range for
1103 * @return array
1105 function get_valid_calendar_event_timestart_range(\calendar_event $event) {
1106 $instance = $this->get_instance();
1107 $submissionsfromdate = $instance->allowsubmissionsfromdate;
1108 $cutoffdate = $instance->cutoffdate;
1109 $duedate = $instance->duedate;
1110 $gradingduedate = $instance->gradingduedate;
1111 $mindate = null;
1112 $maxdate = null;
1114 if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
1115 // This check is in here because due date events are currently
1116 // the only events that can be overridden, so we can save a DB
1117 // query if we don't bother checking other events.
1118 if ($this->is_override_calendar_event($event)) {
1119 // This is an override event so there is no valid timestart
1120 // range to set it to.
1121 return [false, false];
1124 if ($submissionsfromdate) {
1125 $mindate = [
1126 $submissionsfromdate,
1127 get_string('duedatevalidation', 'assign'),
1131 if ($cutoffdate) {
1132 $maxdate = [
1133 $cutoffdate,
1134 get_string('cutoffdatevalidation', 'assign'),
1138 if ($gradingduedate) {
1139 // If we don't have a cutoff date or we've got a grading due date
1140 // that is earlier than the cutoff then we should use that as the
1141 // upper limit for the due date.
1142 if (!$cutoffdate || $gradingduedate < $cutoffdate) {
1143 $maxdate = [
1144 $gradingduedate,
1145 get_string('gradingdueduedatevalidation', 'assign'),
1149 } else if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) {
1150 if ($duedate) {
1151 $mindate = [
1152 $duedate,
1153 get_string('gradingdueduedatevalidation', 'assign'),
1155 } else if ($submissionsfromdate) {
1156 $mindate = [
1157 $submissionsfromdate,
1158 get_string('gradingduefromdatevalidation', 'assign'),
1163 return [$mindate, $maxdate];
1167 * Actual implementation of the reset course functionality, delete all the
1168 * assignment submissions for course $data->courseid.
1170 * @param stdClass $data the data submitted from the reset course.
1171 * @return array status array
1173 public function reset_userdata($data) {
1174 global $CFG, $DB;
1176 $componentstr = get_string('modulenameplural', 'assign');
1177 $status = array();
1179 $fs = get_file_storage();
1180 if (!empty($data->reset_assign_submissions)) {
1181 // Delete files associated with this assignment.
1182 foreach ($this->submissionplugins as $plugin) {
1183 $fileareas = array();
1184 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1185 $fileareas = $plugin->get_file_areas();
1186 foreach ($fileareas as $filearea => $notused) {
1187 $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1190 if (!$plugin->delete_instance()) {
1191 $status[] = array('component'=>$componentstr,
1192 'item'=>get_string('deleteallsubmissions', 'assign'),
1193 'error'=>$plugin->get_error());
1197 foreach ($this->feedbackplugins as $plugin) {
1198 $fileareas = array();
1199 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type();
1200 $fileareas = $plugin->get_file_areas();
1201 foreach ($fileareas as $filearea => $notused) {
1202 $fs->delete_area_files($this->context->id, $plugincomponent, $filearea);
1205 if (!$plugin->delete_instance()) {
1206 $status[] = array('component'=>$componentstr,
1207 'item'=>get_string('deleteallsubmissions', 'assign'),
1208 'error'=>$plugin->get_error());
1212 $assignids = $DB->get_records('assign', array('course' => $data->courseid), '', 'id');
1213 list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids));
1215 $DB->delete_records_select('assign_submission', "assignment $sql", $params);
1216 $DB->delete_records_select('assign_user_flags', "assignment $sql", $params);
1218 $status[] = array('component'=>$componentstr,
1219 'item'=>get_string('deleteallsubmissions', 'assign'),
1220 'error'=>false);
1222 if (!empty($data->reset_gradebook_grades)) {
1223 $DB->delete_records_select('assign_grades', "assignment $sql", $params);
1224 // Remove all grades from gradebook.
1225 require_once($CFG->dirroot.'/mod/assign/lib.php');
1226 assign_reset_gradebook($data->courseid);
1229 // Reset revealidentities for assign if blindmarking is enabled.
1230 if ($this->get_instance()->blindmarking) {
1231 $DB->set_field('assign', 'revealidentities', 0, array('id' => $this->get_instance()->id));
1235 $purgeoverrides = false;
1237 // Remove user overrides.
1238 if (!empty($data->reset_assign_user_overrides)) {
1239 $DB->delete_records_select('assign_overrides',
1240 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid));
1241 $status[] = array(
1242 'component' => $componentstr,
1243 'item' => get_string('useroverridesdeleted', 'assign'),
1244 'error' => false);
1245 $purgeoverrides = true;
1247 // Remove group overrides.
1248 if (!empty($data->reset_assign_group_overrides)) {
1249 $DB->delete_records_select('assign_overrides',
1250 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid));
1251 $status[] = array(
1252 'component' => $componentstr,
1253 'item' => get_string('groupoverridesdeleted', 'assign'),
1254 'error' => false);
1255 $purgeoverrides = true;
1258 // Updating dates - shift may be negative too.
1259 if ($data->timeshift) {
1260 $DB->execute("UPDATE {assign_overrides}
1261 SET allowsubmissionsfromdate = allowsubmissionsfromdate + ?
1262 WHERE assignid = ? AND allowsubmissionsfromdate <> 0",
1263 array($data->timeshift, $this->get_instance()->id));
1264 $DB->execute("UPDATE {assign_overrides}
1265 SET duedate = duedate + ?
1266 WHERE assignid = ? AND duedate <> 0",
1267 array($data->timeshift, $this->get_instance()->id));
1268 $DB->execute("UPDATE {assign_overrides}
1269 SET cutoffdate = cutoffdate + ?
1270 WHERE assignid =? AND cutoffdate <> 0",
1271 array($data->timeshift, $this->get_instance()->id));
1273 $purgeoverrides = true;
1275 // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
1276 // See MDL-9367.
1277 shift_course_mod_dates('assign',
1278 array('duedate', 'allowsubmissionsfromdate', 'cutoffdate'),
1279 $data->timeshift,
1280 $data->courseid, $this->get_instance()->id);
1281 $status[] = array('component'=>$componentstr,
1282 'item'=>get_string('datechanged'),
1283 'error'=>false);
1286 if ($purgeoverrides) {
1287 cache::make('mod_assign', 'overrides')->purge();
1290 return $status;
1294 * Update the settings for a single plugin.
1296 * @param assign_plugin $plugin The plugin to update
1297 * @param stdClass $formdata The form data
1298 * @return bool false if an error occurs
1300 protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) {
1301 if ($plugin->is_visible()) {
1302 $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1303 if (!empty($formdata->$enabledname)) {
1304 $plugin->enable();
1305 if (!$plugin->save_settings($formdata)) {
1306 throw new \moodle_exception($plugin->get_error());
1307 return false;
1309 } else {
1310 $plugin->disable();
1313 return true;
1317 * Update the gradebook information for this assignment.
1319 * @param bool $reset If true, will reset all grades in the gradbook for this assignment
1320 * @param int $coursemoduleid This is required because it might not exist in the database yet
1321 * @return bool
1323 public function update_gradebook($reset, $coursemoduleid) {
1324 global $CFG;
1326 require_once($CFG->dirroot.'/mod/assign/lib.php');
1327 $assign = clone $this->get_instance();
1328 $assign->cmidnumber = $coursemoduleid;
1330 // Set assign gradebook feedback plugin status (enabled and visible).
1331 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
1333 $param = null;
1334 if ($reset) {
1335 $param = 'reset';
1338 return assign_grade_item_update($assign, $param);
1342 * Get the marking table page size
1344 * @return integer
1346 public function get_assign_perpage() {
1347 $perpage = (int) get_user_preferences('assign_perpage', 10);
1348 $adminconfig = $this->get_admin_config();
1349 $maxperpage = -1;
1350 if (isset($adminconfig->maxperpage)) {
1351 $maxperpage = $adminconfig->maxperpage;
1353 if (isset($maxperpage) &&
1354 $maxperpage != -1 &&
1355 ($perpage == -1 || $perpage > $maxperpage)) {
1356 $perpage = $maxperpage;
1358 return $perpage;
1362 * Load and cache the admin config for this module.
1364 * @return stdClass the plugin config
1366 public function get_admin_config() {
1367 if ($this->adminconfig) {
1368 return $this->adminconfig;
1370 $this->adminconfig = get_config('assign');
1371 return $this->adminconfig;
1375 * Update the calendar entries for this assignment.
1377 * @param int $coursemoduleid - Required to pass this in because it might
1378 * not exist in the database yet.
1379 * @return bool
1381 public function update_calendar($coursemoduleid) {
1382 global $DB, $CFG;
1383 require_once($CFG->dirroot.'/calendar/lib.php');
1385 // Special case for add_instance as the coursemodule has not been set yet.
1386 $instance = $this->get_instance();
1388 // Start with creating the event.
1389 $event = new stdClass();
1390 $event->modulename = 'assign';
1391 $event->courseid = $instance->course;
1392 $event->groupid = 0;
1393 $event->userid = 0;
1394 $event->instance = $instance->id;
1395 $event->type = CALENDAR_EVENT_TYPE_ACTION;
1397 // Convert the links to pluginfile. It is a bit hacky but at this stage the files
1398 // might not have been saved in the module area yet.
1399 $intro = $instance->intro;
1400 if ($draftid = file_get_submitted_draft_itemid('introeditor')) {
1401 $intro = file_rewrite_urls_to_pluginfile($intro, $draftid);
1404 // We need to remove the links to files as the calendar is not ready
1405 // to support module events with file areas.
1406 $intro = strip_pluginfile_content($intro);
1407 if ($this->show_intro()) {
1408 $event->description = array(
1409 'text' => $intro,
1410 'format' => $instance->introformat
1412 } else {
1413 $event->description = array(
1414 'text' => '',
1415 'format' => $instance->introformat
1419 $eventtype = ASSIGN_EVENT_TYPE_DUE;
1420 if ($instance->duedate) {
1421 $event->name = get_string('calendardue', 'assign', $instance->name);
1422 $event->eventtype = $eventtype;
1423 $event->timestart = $instance->duedate;
1424 $event->timesort = $instance->duedate;
1425 $select = "modulename = :modulename
1426 AND instance = :instance
1427 AND eventtype = :eventtype
1428 AND groupid = 0
1429 AND courseid <> 0";
1430 $params = array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype);
1431 $event->id = $DB->get_field_select('event', 'id', $select, $params);
1433 // Now process the event.
1434 if ($event->id) {
1435 $calendarevent = calendar_event::load($event->id);
1436 $calendarevent->update($event, false);
1437 } else {
1438 calendar_event::create($event, false);
1440 } else {
1441 $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1442 'eventtype' => $eventtype));
1445 $eventtype = ASSIGN_EVENT_TYPE_GRADINGDUE;
1446 if ($instance->gradingduedate) {
1447 $event->name = get_string('calendargradingdue', 'assign', $instance->name);
1448 $event->eventtype = $eventtype;
1449 $event->timestart = $instance->gradingduedate;
1450 $event->timesort = $instance->gradingduedate;
1451 $event->id = $DB->get_field('event', 'id', array('modulename' => 'assign',
1452 'instance' => $instance->id, 'eventtype' => $event->eventtype));
1454 // Now process the event.
1455 if ($event->id) {
1456 $calendarevent = calendar_event::load($event->id);
1457 $calendarevent->update($event, false);
1458 } else {
1459 calendar_event::create($event, false);
1461 } else {
1462 $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id,
1463 'eventtype' => $eventtype));
1466 return true;
1470 * Update this instance in the database.
1472 * @param stdClass $formdata - the data submitted from the form
1473 * @return bool false if an error occurs
1475 public function update_instance($formdata) {
1476 global $DB;
1477 $adminconfig = $this->get_admin_config();
1479 $update = new stdClass();
1480 $update->id = $formdata->instance;
1481 $update->name = $formdata->name;
1482 $update->timemodified = time();
1483 $update->course = $formdata->course;
1484 $update->intro = $formdata->intro;
1485 $update->introformat = $formdata->introformat;
1486 $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription);
1487 if (isset($formdata->activityeditor)) {
1488 $update->activity = $this->save_editor_draft_files($formdata);
1489 $update->activityformat = $formdata->activityeditor['format'];
1491 if (isset($formdata->submissionattachments)) {
1492 $update->submissionattachments = $formdata->submissionattachments;
1494 $update->submissiondrafts = $formdata->submissiondrafts;
1495 $update->requiresubmissionstatement = $formdata->requiresubmissionstatement;
1496 $update->sendnotifications = $formdata->sendnotifications;
1497 $update->sendlatenotifications = $formdata->sendlatenotifications;
1498 $update->sendstudentnotifications = $adminconfig->sendstudentnotifications;
1499 if (isset($formdata->sendstudentnotifications)) {
1500 $update->sendstudentnotifications = $formdata->sendstudentnotifications;
1502 $update->duedate = $formdata->duedate;
1503 $update->cutoffdate = $formdata->cutoffdate;
1504 if (isset($formdata->timelimit)) {
1505 $update->timelimit = $formdata->timelimit;
1507 $update->gradingduedate = $formdata->gradingduedate;
1508 $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
1509 $update->grade = $formdata->grade;
1510 if (!empty($formdata->completionunlocked)) {
1511 $update->completionsubmit = !empty($formdata->completionsubmit);
1513 $update->teamsubmission = $formdata->teamsubmission;
1514 $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit;
1515 if (isset($formdata->teamsubmissiongroupingid)) {
1516 $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid;
1518 if (isset($formdata->hidegrader)) {
1519 $update->hidegrader = $formdata->hidegrader;
1521 $update->blindmarking = $formdata->blindmarking;
1522 $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE;
1523 if (!empty($formdata->attemptreopenmethod)) {
1524 $update->attemptreopenmethod = $formdata->attemptreopenmethod;
1526 if (!empty($formdata->maxattempts)) {
1527 $update->maxattempts = $formdata->maxattempts;
1529 if (isset($formdata->preventsubmissionnotingroup)) {
1530 $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup;
1532 $update->markingworkflow = $formdata->markingworkflow;
1533 $update->markingallocation = $formdata->markingallocation;
1534 if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
1535 $update->markingallocation = 0;
1538 $result = $DB->update_record('assign', $update);
1539 $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
1541 $this->save_intro_draft_files($formdata);
1543 // Load the assignment so the plugins have access to it.
1545 // Call save_settings hook for submission plugins.
1546 foreach ($this->submissionplugins as $plugin) {
1547 if (!$this->update_plugin_instance($plugin, $formdata)) {
1548 throw new \moodle_exception($plugin->get_error());
1549 return false;
1552 foreach ($this->feedbackplugins as $plugin) {
1553 if (!$this->update_plugin_instance($plugin, $formdata)) {
1554 throw new \moodle_exception($plugin->get_error());
1555 return false;
1559 $this->update_calendar($this->get_course_module()->id);
1560 $completionexpected = (!empty($formdata->completionexpected)) ? $formdata->completionexpected : null;
1561 \core_completion\api::update_completion_date_event($this->get_course_module()->id, 'assign', $this->instance,
1562 $completionexpected);
1563 $this->update_gradebook(false, $this->get_course_module()->id);
1565 $update = new stdClass();
1566 $update->id = $this->get_instance()->id;
1567 $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0;
1568 $DB->update_record('assign', $update);
1570 return $result;
1574 * Save the attachments in the intro description.
1576 * @param stdClass $formdata
1578 protected function save_intro_draft_files($formdata) {
1579 if (isset($formdata->introattachments)) {
1580 file_save_draft_area_files($formdata->introattachments, $this->get_context()->id,
1581 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
1586 * Save the attachments in the editor description.
1588 * @param stdClass $formdata
1590 protected function save_editor_draft_files($formdata): string {
1591 $text = '';
1592 if (isset($formdata->activityeditor)) {
1593 $text = $formdata->activityeditor['text'];
1594 if (isset($formdata->activityeditor['itemid'])) {
1595 $text = file_save_draft_area_files($formdata->activityeditor['itemid'], $this->get_context()->id,
1596 'mod_assign', ASSIGN_ACTIVITYATTACHMENT_FILEAREA,
1597 0, array('subdirs' => true), $formdata->activityeditor['text']);
1600 return $text;
1605 * Add elements in grading plugin form.
1607 * @param mixed $grade stdClass|null
1608 * @param MoodleQuickForm $mform
1609 * @param stdClass $data
1610 * @param int $userid - The userid we are grading
1611 * @return void
1613 protected function add_plugin_grade_elements($grade, MoodleQuickForm $mform, stdClass $data, $userid) {
1614 foreach ($this->feedbackplugins as $plugin) {
1615 if ($plugin->is_enabled() && $plugin->is_visible()) {
1616 $plugin->get_form_elements_for_user($grade, $mform, $data, $userid);
1624 * Add one plugins settings to edit plugin form.
1626 * @param assign_plugin $plugin The plugin to add the settings from
1627 * @param MoodleQuickForm $mform The form to add the configuration settings to.
1628 * This form is modified directly (not returned).
1629 * @param array $pluginsenabled A list of form elements to be added to a group.
1630 * The new element is added to this array by this function.
1631 * @return void
1633 protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) {
1634 global $CFG;
1635 if ($plugin->is_visible() && !$plugin->is_configurable() && $plugin->is_enabled()) {
1636 $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1637 $pluginsenabled[] = $mform->createElement('hidden', $name, 1);
1638 $mform->setType($name, PARAM_BOOL);
1639 $plugin->get_settings($mform);
1640 } else if ($plugin->is_visible() && $plugin->is_configurable()) {
1641 $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled';
1642 $label = $plugin->get_name();
1643 $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label);
1644 $helpicon = $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type());
1645 $pluginsenabled[] = $mform->createElement('static', '', '', $helpicon);
1647 $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default');
1648 if ($plugin->get_config('enabled') !== false) {
1649 $default = $plugin->is_enabled();
1651 $mform->setDefault($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled', $default);
1653 $plugin->get_settings($mform);
1659 * Add settings to edit plugin form.
1661 * @param MoodleQuickForm $mform The form to add the configuration settings to.
1662 * This form is modified directly (not returned).
1663 * @return void
1665 public function add_all_plugin_settings(MoodleQuickForm $mform) {
1666 $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign'));
1668 $submissionpluginsenabled = array();
1669 $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false);
1670 foreach ($this->submissionplugins as $plugin) {
1671 $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled);
1673 $group->setElements($submissionpluginsenabled);
1675 $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign'));
1676 $feedbackpluginsenabled = array();
1677 $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false);
1678 foreach ($this->feedbackplugins as $plugin) {
1679 $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled);
1681 $group->setElements($feedbackpluginsenabled);
1682 $mform->setExpanded('submissiontypes');
1686 * Allow each plugin an opportunity to update the defaultvalues
1687 * passed in to the settings form (needed to set up draft areas for
1688 * editor and filemanager elements)
1690 * @param array $defaultvalues
1692 public function plugin_data_preprocessing(&$defaultvalues) {
1693 foreach ($this->submissionplugins as $plugin) {
1694 if ($plugin->is_visible()) {
1695 $plugin->data_preprocessing($defaultvalues);
1698 foreach ($this->feedbackplugins as $plugin) {
1699 if ($plugin->is_visible()) {
1700 $plugin->data_preprocessing($defaultvalues);
1706 * Get the name of the current module.
1708 * @return string the module name (Assignment)
1710 protected function get_module_name() {
1711 if (isset(self::$modulename)) {
1712 return self::$modulename;
1714 self::$modulename = get_string('modulename', 'assign');
1715 return self::$modulename;
1719 * Get the plural name of the current module.
1721 * @return string the module name plural (Assignments)
1723 protected function get_module_name_plural() {
1724 if (isset(self::$modulenameplural)) {
1725 return self::$modulenameplural;
1727 self::$modulenameplural = get_string('modulenameplural', 'assign');
1728 return self::$modulenameplural;
1732 * Has this assignment been constructed from an instance?
1734 * @return bool
1736 public function has_instance() {
1737 return $this->instance || $this->get_course_module();
1741 * Get the settings for the current instance of this assignment.
1743 * @return stdClass The settings
1744 * @throws dml_exception
1746 public function get_default_instance() {
1747 global $DB;
1748 if (!$this->instance && $this->get_course_module()) {
1749 $params = array('id' => $this->get_course_module()->instance);
1750 $this->instance = $DB->get_record('assign', $params, '*', MUST_EXIST);
1752 $this->userinstances = [];
1754 return $this->instance;
1758 * Get the settings for the current instance of this assignment
1759 * @param int|null $userid the id of the user to load the assign instance for.
1760 * @return stdClass The settings
1762 public function get_instance(int $userid = null) : stdClass {
1763 global $USER;
1764 $userid = $userid ?? $USER->id;
1766 $this->instance = $this->get_default_instance();
1768 // If we have the user instance already, just return it.
1769 if (isset($this->userinstances[$userid])) {
1770 return $this->userinstances[$userid];
1773 // Calculate properties which vary per user.
1774 $this->userinstances[$userid] = $this->calculate_properties($this->instance, $userid);
1775 return $this->userinstances[$userid];
1779 * Calculates and updates various properties based on the specified user.
1781 * @param stdClass $record the raw assign record.
1782 * @param int $userid the id of the user to calculate the properties for.
1783 * @return stdClass a new record having calculated properties.
1785 private function calculate_properties(\stdClass $record, int $userid) : \stdClass {
1786 $record = clone ($record);
1788 // Relative dates.
1789 if (!empty($record->duedate)) {
1790 $course = $this->get_course();
1791 $usercoursedates = course_get_course_dates_for_user_id($course, $userid);
1792 if ($usercoursedates['start']) {
1793 $userprops = ['duedate' => $record->duedate + $usercoursedates['startoffset']];
1794 $record = (object) array_merge((array) $record, (array) $userprops);
1797 return $record;
1801 * Get the primary grade item for this assign instance.
1803 * @return grade_item The grade_item record
1805 public function get_grade_item() {
1806 if ($this->gradeitem) {
1807 return $this->gradeitem;
1809 $instance = $this->get_instance();
1810 $params = array('itemtype' => 'mod',
1811 'itemmodule' => 'assign',
1812 'iteminstance' => $instance->id,
1813 'courseid' => $instance->course,
1814 'itemnumber' => 0);
1815 $this->gradeitem = grade_item::fetch($params);
1816 if (!$this->gradeitem) {
1817 throw new coding_exception('Improper use of the assignment class. ' .
1818 'Cannot load the grade item.');
1820 return $this->gradeitem;
1824 * Get the context of the current course.
1826 * @return mixed context|null The course context
1828 public function get_course_context() {
1829 if (!$this->context && !$this->course) {
1830 throw new coding_exception('Improper use of the assignment class. ' .
1831 'Cannot load the course context.');
1833 if ($this->context) {
1834 return $this->context->get_course_context();
1835 } else {
1836 return context_course::instance($this->course->id);
1842 * Get the current course module.
1844 * @return cm_info|null The course module or null if not known
1846 public function get_course_module() {
1847 if ($this->coursemodule) {
1848 return $this->coursemodule;
1850 if (!$this->context) {
1851 return null;
1854 if ($this->context->contextlevel == CONTEXT_MODULE) {
1855 $modinfo = get_fast_modinfo($this->get_course());
1856 $this->coursemodule = $modinfo->get_cm($this->context->instanceid);
1857 return $this->coursemodule;
1859 return null;
1863 * Get context module.
1865 * @return context
1867 public function get_context() {
1868 return $this->context;
1872 * Get the current course.
1874 * @return mixed stdClass|null The course
1876 public function get_course() {
1877 global $DB;
1879 if ($this->course && is_object($this->course)) {
1880 return $this->course;
1883 if (!$this->context) {
1884 return null;
1886 $params = array('id' => $this->get_course_context()->instanceid);
1887 $this->course = $DB->get_record('course', $params, '*', MUST_EXIST);
1889 return $this->course;
1893 * Count the number of intro attachments.
1895 * @return int
1897 protected function count_attachments() {
1899 $fs = get_file_storage();
1900 $files = $fs->get_area_files($this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA,
1901 0, 'id', false);
1903 return count($files);
1907 * Are there any intro attachments to display?
1909 * @return boolean
1911 protected function has_visible_attachments() {
1912 return ($this->count_attachments() > 0);
1916 * Check if the intro attachments should be provided to the user.
1918 * @param int $userid User id.
1919 * @return bool
1921 public function should_provide_intro_attachments(int $userid): bool {
1922 $instance = $this->get_instance($userid);
1924 // Check if user has permission to view attachments regardless of assignment settings.
1925 if (has_capability('moodle/course:manageactivities', $this->get_context())) {
1926 return true;
1929 // If assignment does not show intro, we never provide intro attachments.
1930 if (!$this->show_intro()) {
1931 return false;
1934 // If intro attachments should only be shown when submission is started, check if there is an open submission.
1935 if (!empty($instance->submissionattachments) && !$this->submissions_open($userid, true)) {
1936 return false;
1939 return true;
1943 * Return a grade in user-friendly form, whether it's a scale or not.
1945 * @param mixed $grade int|null
1946 * @param boolean $editing Are we allowing changes to this grade?
1947 * @param int $userid The user id the grade belongs to
1948 * @param int $modified Timestamp from when the grade was last modified
1949 * @return string User-friendly representation of grade
1951 public function display_grade($grade, $editing, $userid=0, $modified=0) {
1952 global $DB;
1954 static $scalegrades = array();
1956 $o = '';
1958 if ($this->get_instance()->grade >= 0) {
1959 // Normal number.
1960 if ($editing && $this->get_instance()->grade > 0) {
1961 if ($grade < 0) {
1962 $displaygrade = '';
1963 } else {
1964 $displaygrade = format_float($grade, $this->get_grade_item()->get_decimals());
1966 $o .= '<label class="accesshide" for="quickgrade_' . $userid . '">' .
1967 get_string('usergrade', 'assign') .
1968 '</label>';
1969 $o .= '<input type="text"
1970 id="quickgrade_' . $userid . '"
1971 name="quickgrade_' . $userid . '"
1972 value="' . $displaygrade . '"
1973 size="6"
1974 maxlength="10"
1975 class="quickgrade"/>';
1976 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $this->get_grade_item()->get_decimals());
1977 return $o;
1978 } else {
1979 if ($grade == -1 || $grade === null) {
1980 $o .= '-';
1981 } else {
1982 $item = $this->get_grade_item();
1983 $o .= grade_format_gradevalue($grade, $item);
1984 if ($item->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) {
1985 // If displaying the raw grade, also display the total value.
1986 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, $item->get_decimals());
1989 return $o;
1992 } else {
1993 // Scale.
1994 if (empty($this->cache['scale'])) {
1995 if ($scale = $DB->get_record('scale', array('id'=>-($this->get_instance()->grade)))) {
1996 $this->cache['scale'] = make_menu_from_list($scale->scale);
1997 } else {
1998 $o .= '-';
1999 return $o;
2002 if ($editing) {
2003 $o .= '<label class="accesshide"
2004 for="quickgrade_' . $userid . '">' .
2005 get_string('usergrade', 'assign') .
2006 '</label>';
2007 $o .= '<select name="quickgrade_' . $userid . '" class="quickgrade">';
2008 $o .= '<option value="-1">' . get_string('nograde') . '</option>';
2009 foreach ($this->cache['scale'] as $optionid => $option) {
2010 $selected = '';
2011 if ($grade == $optionid) {
2012 $selected = 'selected="selected"';
2014 $o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>';
2016 $o .= '</select>';
2017 return $o;
2018 } else {
2019 $scaleid = (int)$grade;
2020 if (isset($this->cache['scale'][$scaleid])) {
2021 $o .= $this->cache['scale'][$scaleid];
2022 return $o;
2024 $o .= '-';
2025 return $o;
2031 * Get the submission status/grading status for all submissions in this assignment for the
2032 * given paticipants.
2034 * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
2035 * If this is a group assignment, group info is also returned.
2037 * @param array $participants an associative array where the key is the participant id and
2038 * the value is the participant record.
2039 * @return array an associative array where the key is the participant id and the value is
2040 * the participant record.
2042 private function get_submission_info_for_participants($participants) {
2043 global $DB;
2045 if (empty($participants)) {
2046 return $participants;
2049 list($insql, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED);
2051 $assignid = $this->get_instance()->id;
2052 $params['assignmentid1'] = $assignid;
2053 $params['assignmentid2'] = $assignid;
2054 $params['assignmentid3'] = $assignid;
2056 $fields = 'SELECT u.id, s.status, s.timemodified AS stime, g.timemodified AS gtime, g.grade, uf.extensionduedate';
2057 $from = ' FROM {user} u
2058 LEFT JOIN {assign_submission} s
2059 ON u.id = s.userid
2060 AND s.assignment = :assignmentid1
2061 AND s.latest = 1
2062 LEFT JOIN {assign_grades} g
2063 ON u.id = g.userid
2064 AND g.assignment = :assignmentid2
2065 AND g.attemptnumber = s.attemptnumber
2066 LEFT JOIN {assign_user_flags} uf
2067 ON u.id = uf.userid
2068 AND uf.assignment = :assignmentid3
2070 $where = ' WHERE u.id ' . $insql;
2072 if (!empty($this->get_instance()->blindmarking)) {
2073 $from .= 'LEFT JOIN {assign_user_mapping} um
2074 ON u.id = um.userid
2075 AND um.assignment = :assignmentid4 ';
2076 $params['assignmentid4'] = $assignid;
2077 $fields .= ', um.id as recordid ';
2080 $sql = "$fields $from $where";
2082 $records = $DB->get_records_sql($sql, $params);
2084 if ($this->get_instance()->teamsubmission) {
2085 // Get all groups.
2086 $allgroups = groups_get_all_groups($this->get_course()->id,
2087 array_keys($participants),
2088 $this->get_instance()->teamsubmissiongroupingid,
2089 'DISTINCT g.id, g.name');
2092 foreach ($participants as $userid => $participant) {
2093 $participants[$userid]->fullname = $this->fullname($participant);
2094 $participants[$userid]->submitted = false;
2095 $participants[$userid]->requiregrading = false;
2096 $participants[$userid]->grantedextension = false;
2097 $participants[$userid]->submissionstatus = '';
2100 foreach ($records as $userid => $submissioninfo) {
2101 // These filters are 100% the same as the ones in the grading table SQL.
2102 $submitted = false;
2103 $requiregrading = false;
2104 $grantedextension = false;
2105 $submissionstatus = !empty($submissioninfo->status) ? $submissioninfo->status : '';
2107 if (!empty($submissioninfo->stime) && $submissioninfo->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
2108 $submitted = true;
2111 if ($submitted && ($submissioninfo->stime >= $submissioninfo->gtime ||
2112 empty($submissioninfo->gtime) ||
2113 $submissioninfo->grade === null)) {
2114 $requiregrading = true;
2117 if (!empty($submissioninfo->extensionduedate)) {
2118 $grantedextension = true;
2121 $participants[$userid]->submitted = $submitted;
2122 $participants[$userid]->requiregrading = $requiregrading;
2123 $participants[$userid]->grantedextension = $grantedextension;
2124 $participants[$userid]->submissionstatus = $submissionstatus;
2125 if ($this->get_instance()->teamsubmission) {
2126 $group = $this->get_submission_group($userid);
2127 if ($group) {
2128 $participants[$userid]->groupid = $group->id;
2129 $participants[$userid]->groupname = $group->name;
2133 return $participants;
2137 * Get the submission status/grading status for all submissions in this assignment.
2138 * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension).
2139 * If this is a group assignment, group info is also returned.
2141 * @param int $currentgroup
2142 * @param boolean $tablesort Apply current user table sorting preferences.
2143 * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'grantedextension',
2144 * 'groupid', 'groupname'
2146 public function list_participants_with_filter_status_and_group($currentgroup, $tablesort = false) {
2147 $participants = $this->list_participants($currentgroup, false, $tablesort);
2149 if (empty($participants)) {
2150 return $participants;
2151 } else {
2152 return $this->get_submission_info_for_participants($participants);
2157 * Return a valid order by segment for list_participants that matches
2158 * the sorting of the current grading table. Not every field is supported,
2159 * we are only concerned with a list of users so we can't search on anything
2160 * that is not part of the user information (like grading statud or last modified stuff).
2162 * @return string Order by clause for list_participants
2164 private function get_grading_sort_sql() {
2165 $usersort = flexible_table::get_sort_for_table('mod_assign_grading');
2166 // TODO Does not support custom user profile fields (MDL-70456).
2167 $userfieldsapi = \core_user\fields::for_identity($this->context, false)->with_userpic();
2168 $userfields = $userfieldsapi->get_required_fields();
2169 $orderfields = explode(',', $usersort);
2170 $validlist = [];
2172 foreach ($orderfields as $orderfield) {
2173 $orderfield = trim($orderfield);
2174 foreach ($userfields as $field) {
2175 $parts = explode(' ', $orderfield);
2176 if ($parts[0] == $field) {
2177 // Prepend the user table prefix and count this as a valid order field.
2178 array_push($validlist, 'u.' . $orderfield);
2182 // Produce a final list.
2183 $result = implode(',', $validlist);
2184 if (empty($result)) {
2185 // Fall back ordering when none has been set.
2186 $result = 'u.lastname, u.firstname, u.id';
2189 return $result;
2193 * Returns array with sql code and parameters returning all ids of users who have submitted an assignment.
2195 * @param int $group The group that the query is for.
2196 * @return array list($sql, $params)
2198 protected function get_submitted_sql($group = 0) {
2199 // We need to guarentee unique table names.
2200 static $i = 0;
2201 $i++;
2202 $prefix = 'sa' . $i . '_';
2203 $params = [
2204 "{$prefix}assignment" => (int) $this->get_instance()->id,
2205 "{$prefix}status" => ASSIGN_SUBMISSION_STATUS_NEW,
2207 $capjoin = get_enrolled_with_capabilities_join($this->context, $prefix, '', $group, $this->show_only_active_users());
2208 $params += $capjoin->params;
2209 $sql = "SELECT {$prefix}s.userid
2210 FROM {assign_submission} {$prefix}s
2211 JOIN {user} {$prefix}u ON {$prefix}u.id = {$prefix}s.userid
2212 $capjoin->joins
2213 WHERE {$prefix}s.assignment = :{$prefix}assignment
2214 AND {$prefix}s.status <> :{$prefix}status
2215 AND $capjoin->wheres";
2216 return array($sql, $params);
2220 * Load a list of users enrolled in the current course with the specified permission and group.
2221 * 0 for no group.
2222 * Apply any current sort filters from the grading table.
2224 * @param int $currentgroup
2225 * @param bool $idsonly
2226 * @param bool $tablesort
2227 * @return array List of user records
2229 public function list_participants($currentgroup, $idsonly, $tablesort = false) {
2230 global $DB, $USER;
2232 // Get the last known sort order for the grading table.
2234 if (empty($currentgroup)) {
2235 $currentgroup = 0;
2238 $key = $this->context->id . '-' . $currentgroup . '-' . $this->show_only_active_users();
2239 if (!isset($this->participants[$key])) {
2240 list($esql, $params) = get_enrolled_sql($this->context, 'mod/assign:submit', $currentgroup,
2241 $this->show_only_active_users());
2242 list($ssql, $sparams) = $this->get_submitted_sql($currentgroup);
2243 $params += $sparams;
2245 $fields = 'u.*';
2246 $orderby = 'u.lastname, u.firstname, u.id';
2248 $additionaljoins = '';
2249 $additionalfilters = '';
2250 $instance = $this->get_instance();
2251 if (!empty($instance->blindmarking)) {
2252 $additionaljoins .= " LEFT JOIN {assign_user_mapping} um
2253 ON u.id = um.userid
2254 AND um.assignment = :assignmentid1
2255 LEFT JOIN {assign_submission} s
2256 ON u.id = s.userid
2257 AND s.assignment = :assignmentid2
2258 AND s.latest = 1
2260 $params['assignmentid1'] = (int) $instance->id;
2261 $params['assignmentid2'] = (int) $instance->id;
2262 $fields .= ', um.id as recordid ';
2264 // Sort by submission time first, then by um.id to sort reliably by the blind marking id.
2265 // Note, different DBs have different ordering of NULL values.
2266 // Therefore we coalesce the current time into the timecreated field, and the max possible integer into
2267 // the ID field.
2268 if (empty($tablesort)) {
2269 $orderby = "COALESCE(s.timecreated, " . time() . ") ASC, COALESCE(s.id, " . PHP_INT_MAX . ") ASC, um.id ASC";
2273 if ($instance->markingworkflow &&
2274 $instance->markingallocation &&
2275 !has_capability('mod/assign:manageallocations', $this->get_context()) &&
2276 has_capability('mod/assign:grade', $this->get_context())) {
2278 $additionaljoins .= ' LEFT JOIN {assign_user_flags} uf
2279 ON u.id = uf.userid
2280 AND uf.assignment = :assignmentid3';
2282 $params['assignmentid3'] = (int) $instance->id;
2284 $additionalfilters .= ' AND uf.allocatedmarker = :markerid';
2285 $params['markerid'] = $USER->id;
2288 $sql = "SELECT $fields
2289 FROM {user} u
2290 JOIN ($esql UNION $ssql) je ON je.id = u.id
2291 $additionaljoins
2292 WHERE u.deleted = 0
2293 $additionalfilters
2294 ORDER BY $orderby";
2296 $users = $DB->get_records_sql($sql, $params);
2298 $cm = $this->get_course_module();
2299 $info = new \core_availability\info_module($cm);
2300 $users = $info->filter_user_list($users);
2302 $this->participants[$key] = $users;
2305 if ($tablesort) {
2306 // Resort the user list according to the grading table sort and filter settings.
2307 $sortedfiltereduserids = $this->get_grading_userid_list(true, '');
2308 $sortedfilteredusers = [];
2309 foreach ($sortedfiltereduserids as $nextid) {
2310 $nextid = intval($nextid);
2311 if (isset($this->participants[$key][$nextid])) {
2312 $sortedfilteredusers[$nextid] = $this->participants[$key][$nextid];
2315 $this->participants[$key] = $sortedfilteredusers;
2318 if ($idsonly) {
2319 $idslist = array();
2320 foreach ($this->participants[$key] as $id => $user) {
2321 $idslist[$id] = new stdClass();
2322 $idslist[$id]->id = $id;
2324 return $idslist;
2326 return $this->participants[$key];
2330 * Load a user if they are enrolled in the current course. Populated with submission
2331 * status for this assignment.
2333 * @param int $userid
2334 * @return null|stdClass user record
2336 public function get_participant($userid) {
2337 global $DB, $USER;
2339 if ($userid == $USER->id) {
2340 $participant = clone ($USER);
2341 } else {
2342 $participant = $DB->get_record('user', array('id' => $userid));
2344 if (!$participant) {
2345 return null;
2348 if (!is_enrolled($this->context, $participant, '', $this->show_only_active_users())) {
2349 return null;
2352 $result = $this->get_submission_info_for_participants(array($participant->id => $participant));
2354 $submissioninfo = $result[$participant->id];
2355 if (!$submissioninfo->submitted && !has_capability('mod/assign:submit', $this->context, $userid)) {
2356 return null;
2359 return $submissioninfo;
2363 * Load a count of valid teams for this assignment.
2365 * @param int $activitygroup Activity active group
2366 * @return int number of valid teams
2368 public function count_teams($activitygroup = 0) {
2370 $count = 0;
2372 $participants = $this->list_participants($activitygroup, true);
2374 // If a team submission grouping id is provided all good as all returned groups
2375 // are the submission teams, but if no team submission grouping was specified
2376 // $groups will contain all participants groups.
2377 if ($this->get_instance()->teamsubmissiongroupingid) {
2379 // We restrict the users to the selected group ones.
2380 $groups = groups_get_all_groups($this->get_course()->id,
2381 array_keys($participants),
2382 $this->get_instance()->teamsubmissiongroupingid,
2383 'DISTINCT g.id, g.name');
2385 $count = count($groups);
2387 // When a specific group is selected we don't count the default group users.
2388 if ($activitygroup == 0) {
2389 if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2390 // See if there are any users in the default group.
2391 $defaultusers = $this->get_submission_group_members(0, true);
2392 if (count($defaultusers) > 0) {
2393 $count += 1;
2396 } else if ($activitygroup != 0 && empty($groups)) {
2397 // Set count to 1 if $groups returns empty.
2398 // It means the group is not part of $this->get_instance()->teamsubmissiongroupingid.
2399 $count = 1;
2401 } else {
2402 // It is faster to loop around participants if no grouping was specified.
2403 $groups = array();
2404 foreach ($participants as $participant) {
2405 if ($group = $this->get_submission_group($participant->id)) {
2406 $groups[$group->id] = true;
2407 } else if (empty($this->get_instance()->preventsubmissionnotingroup)) {
2408 $groups[0] = true;
2412 $count = count($groups);
2415 return $count;
2419 * Load a count of active users enrolled in the current course with the specified permission and group.
2420 * 0 for no group.
2422 * @param int $currentgroup
2423 * @return int number of matching users
2425 public function count_participants($currentgroup) {
2426 return count($this->list_participants($currentgroup, true));
2430 * Load a count of active users submissions in the current module that require grading
2431 * This means the submission modification time is more recent than the
2432 * grading modification time and the status is SUBMITTED.
2434 * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
2435 * @return int number of matching submissions
2437 public function count_submissions_need_grading($currentgroup = null) {
2438 global $DB;
2440 if ($this->get_instance()->teamsubmission) {
2441 // This does not make sense for group assignment because the submission is shared.
2442 return 0;
2445 if ($currentgroup === null) {
2446 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2448 list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true);
2450 $params['assignid'] = $this->get_instance()->id;
2451 $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
2452 $sqlscalegrade = $this->get_instance()->grade < 0 ? ' OR g.grade = -1' : '';
2454 $sql = 'SELECT COUNT(s.userid)
2455 FROM {assign_submission} s
2456 LEFT JOIN {assign_grades} g ON
2457 s.assignment = g.assignment AND
2458 s.userid = g.userid AND
2459 g.attemptnumber = s.attemptnumber
2460 JOIN(' . $esql . ') e ON e.id = s.userid
2461 WHERE
2462 s.latest = 1 AND
2463 s.assignment = :assignid AND
2464 s.timemodified IS NOT NULL AND
2465 s.status = :submitted AND
2466 (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL '
2467 . $sqlscalegrade . ')';
2469 return $DB->count_records_sql($sql, $params);
2473 * Load a count of grades.
2475 * @return int number of grades
2477 public function count_grades() {
2478 global $DB;
2480 if (!$this->has_instance()) {
2481 return 0;
2484 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2485 list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2487 $params['assignid'] = $this->get_instance()->id;
2489 $sql = 'SELECT COUNT(g.userid)
2490 FROM {assign_grades} g
2491 JOIN(' . $esql . ') e ON e.id = g.userid
2492 WHERE g.assignment = :assignid';
2494 return $DB->count_records_sql($sql, $params);
2498 * Load a count of submissions.
2500 * @param bool $includenew When true, also counts the submissions with status 'new'.
2501 * @return int number of submissions
2503 public function count_submissions($includenew = false) {
2504 global $DB;
2506 if (!$this->has_instance()) {
2507 return 0;
2510 $params = array();
2511 $sqlnew = '';
2513 if (!$includenew) {
2514 $sqlnew = ' AND s.status <> :status ';
2515 $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW;
2518 if ($this->get_instance()->teamsubmission) {
2519 // We cannot join on the enrolment tables for group submissions (no userid).
2520 $sql = 'SELECT COUNT(DISTINCT s.groupid)
2521 FROM {assign_submission} s
2522 WHERE
2523 s.assignment = :assignid AND
2524 s.timemodified IS NOT NULL AND
2525 s.userid = :groupuserid' .
2526 $sqlnew;
2528 $params['assignid'] = $this->get_instance()->id;
2529 $params['groupuserid'] = 0;
2530 } else {
2531 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2532 list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
2534 $params = array_merge($params, $enrolparams);
2535 $params['assignid'] = $this->get_instance()->id;
2537 $sql = 'SELECT COUNT(DISTINCT s.userid)
2538 FROM {assign_submission} s
2539 JOIN(' . $esql . ') e ON e.id = s.userid
2540 WHERE
2541 s.assignment = :assignid AND
2542 s.timemodified IS NOT NULL ' .
2543 $sqlnew;
2547 return $DB->count_records_sql($sql, $params);
2551 * Load a count of submissions with a specified status.
2553 * @param string $status The submission status - should match one of the constants
2554 * @param mixed $currentgroup int|null the group for counting (if null the function will determine it)
2555 * @return int number of matching submissions
2557 public function count_submissions_with_status($status, $currentgroup = null) {
2558 global $DB;
2560 if ($currentgroup === null) {
2561 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
2563 list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true);
2565 $params['assignid'] = $this->get_instance()->id;
2566 $params['assignid2'] = $this->get_instance()->id;
2567 $params['submissionstatus'] = $status;
2569 if ($this->get_instance()->teamsubmission) {
2571 $groupsstr = '';
2572 if ($currentgroup != 0) {
2573 // If there is an active group we should only display the current group users groups.
2574 $participants = $this->list_participants($currentgroup, true);
2575 $groups = groups_get_all_groups($this->get_course()->id,
2576 array_keys($participants),
2577 $this->get_instance()->teamsubmissiongroupingid,
2578 'DISTINCT g.id, g.name');
2579 if (empty($groups)) {
2580 // If $groups is empty it means it is not part of $this->get_instance()->teamsubmissiongroupingid.
2581 // All submissions from students that do not belong to any of teamsubmissiongroupingid groups
2582 // count towards groupid = 0. Setting to true as only '0' key matters.
2583 $groups = [true];
2585 list($groupssql, $groupsparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED);
2586 $groupsstr = 's.groupid ' . $groupssql . ' AND';
2587 $params = $params + $groupsparams;
2589 $sql = 'SELECT COUNT(s.groupid)
2590 FROM {assign_submission} s
2591 WHERE
2592 s.latest = 1 AND
2593 s.assignment = :assignid AND
2594 s.timemodified IS NOT NULL AND
2595 s.userid = :groupuserid AND '
2596 . $groupsstr . '
2597 s.status = :submissionstatus';
2598 $params['groupuserid'] = 0;
2599 } else {
2600 $sql = 'SELECT COUNT(s.userid)
2601 FROM {assign_submission} s
2602 JOIN(' . $esql . ') e ON e.id = s.userid
2603 WHERE
2604 s.latest = 1 AND
2605 s.assignment = :assignid AND
2606 s.timemodified IS NOT NULL AND
2607 s.status = :submissionstatus';
2611 return $DB->count_records_sql($sql, $params);
2615 * Utility function to get the userid for every row in the grading table
2616 * so the order can be frozen while we iterate it.
2618 * @param boolean $cached If true, the cached list from the session could be returned.
2619 * @param string $useridlistid String value used for caching the participant list.
2620 * @return array An array of userids
2622 protected function get_grading_userid_list($cached = false, $useridlistid = '') {
2623 global $SESSION;
2625 if ($cached) {
2626 if (empty($useridlistid)) {
2627 $useridlistid = $this->get_useridlist_key_id();
2629 $useridlistkey = $this->get_useridlist_key($useridlistid);
2630 if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) {
2631 $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list(false, '');
2633 return $SESSION->mod_assign_useridlist[$useridlistkey];
2635 $filter = get_user_preferences('assign_filter', '');
2636 $table = new assign_grading_table($this, 0, $filter, 0, false);
2638 $useridlist = $table->get_column_data('userid');
2640 return $useridlist;
2644 * Is user id filtered by user filters and table preferences.
2646 * @param int $userid User id that needs to be checked.
2647 * @return bool
2649 public function is_userid_filtered($userid) {
2650 $users = $this->get_grading_userid_list();
2651 return in_array($userid, $users);
2655 * Finds all assignment notifications that have yet to be mailed out, and mails them.
2657 * Cron function to be run periodically according to the moodle cron.
2659 * @return bool
2661 public static function cron() {
2662 global $DB;
2664 // Only ever send a max of one days worth of updates.
2665 $yesterday = time() - (24 * 3600);
2666 $timenow = time();
2667 $task = \core\task\manager::get_scheduled_task(mod_assign\task\cron_task::class);
2668 $lastruntime = $task->get_last_run_time();
2670 // Collect all submissions that require mailing.
2671 // Submissions are included if all are true:
2672 // - The assignment is visible in the gradebook.
2673 // - No previous notification has been sent.
2674 // - The grader was a real user, not an automated process.
2675 // - The grade was updated in the past 24 hours.
2676 // - If marking workflow is enabled, the workflow state is at 'released'.
2677 $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities, a.hidegrader,
2678 g.*, g.timemodified as lastmodified, cm.id as cmid, um.id as recordid
2679 FROM {assign} a
2680 JOIN {assign_grades} g ON g.assignment = a.id
2681 LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid
2682 JOIN {course_modules} cm ON cm.course = a.course AND cm.instance = a.id
2683 JOIN {modules} md ON md.id = cm.module AND md.name = 'assign'
2684 JOIN {grade_items} gri ON gri.iteminstance = a.id AND gri.courseid = a.course AND gri.itemmodule = md.name
2685 LEFT JOIN {assign_user_mapping} um ON g.id = um.userid AND um.assignment = a.id
2686 WHERE (a.markingworkflow = 0 OR (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND
2687 g.grader > 0 AND uf.mailed = 0 AND gri.hidden = 0 AND
2688 g.timemodified >= :yesterday AND g.timemodified <= :today
2689 ORDER BY a.course, cm.id";
2691 $params = array(
2692 'yesterday' => $yesterday,
2693 'today' => $timenow,
2694 'wfreleased' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED,
2696 $submissions = $DB->get_records_sql($sql, $params);
2698 if (!empty($submissions)) {
2700 mtrace('Processing ' . count($submissions) . ' assignment submissions ...');
2702 // Preload courses we are going to need those.
2703 $courseids = array();
2704 foreach ($submissions as $submission) {
2705 $courseids[] = $submission->course;
2708 // Filter out duplicates.
2709 $courseids = array_unique($courseids);
2710 $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
2711 list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
2712 $sql = 'SELECT c.*, ' . $ctxselect .
2713 ' FROM {course} c
2714 LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
2715 WHERE c.id ' . $courseidsql;
2717 $params['contextlevel'] = CONTEXT_COURSE;
2718 $courses = $DB->get_records_sql($sql, $params);
2720 // Clean up... this could go on for a while.
2721 unset($courseids);
2722 unset($ctxselect);
2723 unset($courseidsql);
2724 unset($params);
2726 // Message students about new feedback.
2727 foreach ($submissions as $submission) {
2729 mtrace("Processing assignment submission $submission->id ...");
2731 // Do not cache user lookups - could be too many.
2732 if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) {
2733 mtrace('Could not find user ' . $submission->userid);
2734 continue;
2737 // Use a cache to prevent the same DB queries happening over and over.
2738 if (!array_key_exists($submission->course, $courses)) {
2739 mtrace('Could not find course ' . $submission->course);
2740 continue;
2742 $course = $courses[$submission->course];
2743 if (isset($course->ctxid)) {
2744 // Context has not yet been preloaded. Do so now.
2745 context_helper::preload_from_record($course);
2748 // Override the language and timezone of the "current" user, so that
2749 // mail is customised for the receiver.
2750 \core\cron::setup_user($user, $course);
2752 // Context lookups are already cached.
2753 $coursecontext = context_course::instance($course->id);
2754 if (!is_enrolled($coursecontext, $user->id)) {
2755 $courseshortname = format_string($course->shortname,
2756 true,
2757 array('context' => $coursecontext));
2758 mtrace(fullname($user) . ' not an active participant in ' . $courseshortname);
2759 continue;
2762 if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) {
2763 mtrace('Could not find grader ' . $submission->grader);
2764 continue;
2767 $modinfo = get_fast_modinfo($course, $user->id);
2768 $cm = $modinfo->get_cm($submission->cmid);
2769 // Context lookups are already cached.
2770 $contextmodule = context_module::instance($cm->id);
2772 if (!$cm->uservisible) {
2773 // Hold mail notification for assignments the user cannot access until later.
2774 continue;
2777 // Notify the student. Default to the non-anon version.
2778 $messagetype = 'feedbackavailable';
2779 // Message type needs 'anon' if "hidden grading" is enabled and the student
2780 // doesn't have permission to see the grader.
2781 if ($submission->hidegrader && !has_capability('mod/assign:showhiddengrader', $contextmodule, $user)) {
2782 $messagetype = 'feedbackavailableanon';
2783 // There's no point in having an "anonymous grader" if the notification email
2784 // comes from them. Send the email from the noreply user instead.
2785 $grader = core_user::get_noreply_user();
2788 $eventtype = 'assign_notification';
2789 $updatetime = $submission->lastmodified;
2790 $modulename = get_string('modulename', 'assign');
2792 $uniqueid = 0;
2793 if ($submission->blindmarking && !$submission->revealidentities) {
2794 if (empty($submission->recordid)) {
2795 $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $grader->id);
2796 } else {
2797 $uniqueid = $submission->recordid;
2800 $showusers = $submission->blindmarking && !$submission->revealidentities;
2801 self::send_assignment_notification($grader,
2802 $user,
2803 $messagetype,
2804 $eventtype,
2805 $updatetime,
2806 $cm,
2807 $contextmodule,
2808 $course,
2809 $modulename,
2810 $submission->name,
2811 $showusers,
2812 $uniqueid);
2814 $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
2815 if ($flags) {
2816 $flags->mailed = 1;
2817 $DB->update_record('assign_user_flags', $flags);
2818 } else {
2819 $flags = new stdClass();
2820 $flags->userid = $user->id;
2821 $flags->assignment = $submission->assignment;
2822 $flags->mailed = 1;
2823 $DB->insert_record('assign_user_flags', $flags);
2826 mtrace('Done');
2828 mtrace('Done processing ' . count($submissions) . ' assignment submissions');
2830 \core\cron::setup_user();
2832 // Free up memory just to be sure.
2833 unset($courses);
2836 // Update calendar events to provide a description.
2837 $sql = 'SELECT id
2838 FROM {assign}
2839 WHERE
2840 allowsubmissionsfromdate >= :lastruntime AND
2841 allowsubmissionsfromdate <= :timenow AND
2842 alwaysshowdescription = 0';
2843 $params = array('lastruntime' => $lastruntime, 'timenow' => $timenow);
2844 $newlyavailable = $DB->get_records_sql($sql, $params);
2845 foreach ($newlyavailable as $record) {
2846 $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST);
2847 $context = context_module::instance($cm->id);
2849 $assignment = new assign($context, null, null);
2850 $assignment->update_calendar($cm->id);
2853 return true;
2857 * Mark in the database that this grade record should have an update notification sent by cron.
2859 * @param stdClass $grade a grade record keyed on id
2860 * @param bool $mailedoverride when true, flag notification to be sent again.
2861 * @return bool true for success
2863 public function notify_grade_modified($grade, $mailedoverride = false) {
2864 global $DB;
2866 $flags = $this->get_user_flags($grade->userid, true);
2867 if ($flags->mailed != 1 || $mailedoverride) {
2868 $flags->mailed = 0;
2871 return $this->update_user_flags($flags);
2875 * Update user flags for this user in this assignment.
2877 * @param stdClass $flags a flags record keyed on id
2878 * @return bool true for success
2880 public function update_user_flags($flags) {
2881 global $DB;
2882 if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) {
2883 return false;
2886 $result = $DB->update_record('assign_user_flags', $flags);
2887 return $result;
2891 * Update a grade in the grade table for the assignment and in the gradebook.
2893 * @param stdClass $grade a grade record keyed on id
2894 * @param bool $reopenattempt If the attempt reopen method is manual, allow another attempt at this assignment.
2895 * @return bool true for success
2897 public function update_grade($grade, $reopenattempt = false) {
2898 global $DB;
2900 $grade->timemodified = time();
2902 if (!empty($grade->workflowstate)) {
2903 $validstates = $this->get_marking_workflow_states_for_current_user();
2904 if (!array_key_exists($grade->workflowstate, $validstates)) {
2905 return false;
2909 if ($grade->grade && $grade->grade != -1) {
2910 if ($this->get_instance()->grade > 0) {
2911 if (!is_numeric($grade->grade)) {
2912 return false;
2913 } else if ($grade->grade > $this->get_instance()->grade) {
2914 return false;
2915 } else if ($grade->grade < 0) {
2916 return false;
2918 } else {
2919 // This is a scale.
2920 if ($scale = $DB->get_record('scale', array('id' => -($this->get_instance()->grade)))) {
2921 $scaleoptions = make_menu_from_list($scale->scale);
2922 if (!array_key_exists((int) $grade->grade, $scaleoptions)) {
2923 return false;
2929 if (empty($grade->attemptnumber)) {
2930 // Set it to the default.
2931 $grade->attemptnumber = 0;
2933 $DB->update_record('assign_grades', $grade);
2935 $submission = null;
2936 if ($this->get_instance()->teamsubmission) {
2937 if (isset($this->mostrecentteamsubmission)) {
2938 $submission = $this->mostrecentteamsubmission;
2939 } else {
2940 $submission = $this->get_group_submission($grade->userid, 0, false);
2942 } else {
2943 $submission = $this->get_user_submission($grade->userid, false);
2946 // Only push to gradebook if the update is for the most recent attempt.
2947 if ($submission && $submission->attemptnumber != $grade->attemptnumber) {
2948 return true;
2951 if ($this->gradebook_item_update(null, $grade)) {
2952 \mod_assign\event\submission_graded::create_from_grade($this, $grade)->trigger();
2955 // If the conditions are met, allow another attempt.
2956 if ($submission) {
2957 $isreopened = $this->reopen_submission_if_required($grade->userid,
2958 $submission,
2959 $reopenattempt);
2960 if ($isreopened) {
2961 $completion = new completion_info($this->get_course());
2962 if ($completion->is_enabled($this->get_course_module()) &&
2963 $this->get_instance()->completionsubmit) {
2964 $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $grade->userid);
2969 return true;
2973 * View the grant extension date page.
2975 * Uses url parameters 'userid'
2976 * or from parameter 'selectedusers'
2978 * @param moodleform $mform - Used for validation of the submitted data
2979 * @return string
2981 protected function view_grant_extension($mform) {
2982 global $CFG;
2983 require_once($CFG->dirroot . '/mod/assign/extensionform.php');
2985 $o = '';
2987 $data = new stdClass();
2988 $data->id = $this->get_course_module()->id;
2990 $formparams = array(
2991 'instance' => $this->get_instance(),
2992 'assign' => $this
2995 $users = optional_param('userid', 0, PARAM_INT);
2996 if (!$users) {
2997 $users = required_param('selectedusers', PARAM_SEQUENCE);
2999 $userlist = explode(',', $users);
3001 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
3002 $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
3003 foreach ($userlist as $userid) {
3004 // To validate extension date with users overrides.
3005 $override = $this->override_exists($userid);
3006 foreach ($keys as $key) {
3007 if ($override->{$key}) {
3008 if ($maxoverride[$key] < $override->{$key}) {
3009 $maxoverride[$key] = $override->{$key};
3011 } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
3012 $maxoverride[$key] = $this->get_instance()->{$key};
3016 foreach ($keys as $key) {
3017 if ($maxoverride[$key]) {
3018 $this->get_instance()->{$key} = $maxoverride[$key];
3022 $formparams['userlist'] = $userlist;
3024 $data->selectedusers = $users;
3025 $data->userid = 0;
3027 if (empty($mform)) {
3028 $mform = new mod_assign_extension_form(null, $formparams);
3030 $mform->set_data($data);
3031 $header = new assign_header($this->get_instance(),
3032 $this->get_context(),
3033 $this->show_intro(),
3034 $this->get_course_module()->id,
3035 get_string('grantextension', 'assign'));
3036 $o .= $this->get_renderer()->render($header);
3037 $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform));
3038 $o .= $this->view_footer();
3039 return $o;
3043 * Get a list of the users in the same group as this user.
3045 * @param int $groupid The id of the group whose members we want or 0 for the default group
3046 * @param bool $onlyids Whether to retrieve only the user id's
3047 * @param bool $excludesuspended Whether to exclude suspended users
3048 * @return array The users (possibly id's only)
3050 public function get_submission_group_members($groupid, $onlyids, $excludesuspended = false) {
3051 $members = array();
3052 if ($groupid != 0) {
3053 $allusers = $this->list_participants($groupid, $onlyids);
3054 foreach ($allusers as $user) {
3055 if ($this->get_submission_group($user->id)) {
3056 $members[] = $user;
3059 } else {
3060 $allusers = $this->list_participants(null, $onlyids);
3061 foreach ($allusers as $user) {
3062 if ($this->get_submission_group($user->id) == null) {
3063 $members[] = $user;
3067 // Exclude suspended users, if user can't see them.
3068 if ($excludesuspended || !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
3069 foreach ($members as $key => $member) {
3070 if (!$this->is_active_user($member->id)) {
3071 unset($members[$key]);
3076 return $members;
3080 * Get a list of the users in the same group as this user that have not submitted the assignment.
3082 * @param int $groupid The id of the group whose members we want or 0 for the default group
3083 * @param bool $onlyids Whether to retrieve only the user id's
3084 * @return array The users (possibly id's only)
3086 public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) {
3087 $instance = $this->get_instance();
3088 if (!$instance->teamsubmission || !$instance->requireallteammemberssubmit) {
3089 return array();
3091 $members = $this->get_submission_group_members($groupid, $onlyids);
3093 foreach ($members as $id => $member) {
3094 $submission = $this->get_user_submission($member->id, false);
3095 if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
3096 unset($members[$id]);
3097 } else {
3098 if ($this->is_blind_marking()) {
3099 $members[$id]->alias = get_string('hiddenuser', 'assign') .
3100 $this->get_uniqueid_for_user($id);
3104 return $members;
3108 * Load the group submission object for a particular user, optionally creating it if required.
3110 * @param int $userid The id of the user whose submission we want
3111 * @param int $groupid The id of the group for this user - may be 0 in which
3112 * case it is determined from the userid.
3113 * @param bool $create If set to true a new submission object will be created in the database
3114 * with the status set to "new".
3115 * @param int $attemptnumber - -1 means the latest attempt
3116 * @return stdClass|false The submission
3118 public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) {
3119 global $DB;
3121 if ($groupid == 0) {
3122 $group = $this->get_submission_group($userid);
3123 if ($group) {
3124 $groupid = $group->id;
3128 // Now get the group submission.
3129 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
3130 if ($attemptnumber >= 0) {
3131 $params['attemptnumber'] = $attemptnumber;
3134 // Only return the row with the highest attemptnumber.
3135 $submission = null;
3136 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
3137 if ($submissions) {
3138 $submission = reset($submissions);
3141 if ($submission) {
3142 if ($create) {
3143 $action = optional_param('action', '', PARAM_TEXT);
3144 if ($action == 'editsubmission') {
3145 if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
3146 $submission->timestarted = time();
3147 $DB->update_record('assign_submission', $submission);
3151 return $submission;
3153 if ($create) {
3154 $submission = new stdClass();
3155 $submission->assignment = $this->get_instance()->id;
3156 $submission->userid = 0;
3157 $submission->groupid = $groupid;
3158 $submission->timecreated = time();
3159 $submission->timemodified = $submission->timecreated;
3160 if ($attemptnumber >= 0) {
3161 $submission->attemptnumber = $attemptnumber;
3162 } else {
3163 $submission->attemptnumber = 0;
3165 // Work out if this is the latest submission.
3166 $submission->latest = 0;
3167 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
3168 if ($attemptnumber == -1) {
3169 // This is a new submission so it must be the latest.
3170 $submission->latest = 1;
3171 } else {
3172 // We need to work this out.
3173 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
3174 if ($result) {
3175 $latestsubmission = reset($result);
3177 if (!$latestsubmission || ($attemptnumber == $latestsubmission->attemptnumber)) {
3178 $submission->latest = 1;
3181 $transaction = $DB->start_delegated_transaction();
3182 if ($submission->latest) {
3183 // This is the case when we need to set latest to 0 for all the other attempts.
3184 $DB->set_field('assign_submission', 'latest', 0, $params);
3186 $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
3187 $sid = $DB->insert_record('assign_submission', $submission);
3188 $transaction->allow_commit();
3189 return $DB->get_record('assign_submission', array('id' => $sid));
3191 return false;
3195 * View a summary listing of all assignments in the current course.
3197 * @return string
3199 private function view_course_index() {
3200 global $USER;
3202 $o = '';
3204 $course = $this->get_course();
3205 $strplural = get_string('modulenameplural', 'assign');
3207 if (!$cms = get_coursemodules_in_course('assign', $course->id, 'm.duedate')) {
3208 $o .= $this->get_renderer()->notification(get_string('thereareno', 'moodle', $strplural));
3209 $o .= $this->get_renderer()->continue_button(new moodle_url('/course/view.php', array('id' => $course->id)));
3210 return $o;
3213 $strsectionname = '';
3214 $usesections = course_format_uses_sections($course->format);
3215 $modinfo = get_fast_modinfo($course);
3217 if ($usesections) {
3218 $strsectionname = get_string('sectionname', 'format_'.$course->format);
3219 $sections = $modinfo->get_section_info_all();
3221 $courseindexsummary = new assign_course_index_summary($usesections, $strsectionname);
3223 $timenow = time();
3225 $currentsection = '';
3226 foreach ($modinfo->instances['assign'] as $cm) {
3227 if (!$cm->uservisible) {
3228 continue;
3231 $timedue = $cms[$cm->id]->duedate;
3233 $sectionname = '';
3234 if ($usesections && $cm->sectionnum) {
3235 $sectionname = get_section_name($course, $sections[$cm->sectionnum]);
3238 $submitted = '';
3239 $context = context_module::instance($cm->id);
3241 $assignment = new assign($context, $cm, $course);
3243 // Apply overrides.
3244 $assignment->update_effective_access($USER->id);
3245 $timedue = $assignment->get_instance()->duedate;
3247 if (has_capability('mod/assign:submit', $context) &&
3248 !has_capability('moodle/site:config', $context)) {
3249 $cangrade = false;
3250 if ($assignment->get_instance()->teamsubmission) {
3251 $usersubmission = $assignment->get_group_submission($USER->id, 0, false);
3252 } else {
3253 $usersubmission = $assignment->get_user_submission($USER->id, false);
3256 if (!empty($usersubmission->status)) {
3257 $submitted = get_string('submissionstatus_' . $usersubmission->status, 'assign');
3258 } else {
3259 $submitted = get_string('submissionstatus_', 'assign');
3262 $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $cm->instance, $USER->id);
3263 if (isset($gradinginfo->items[0]->grades[$USER->id]) &&
3264 !$gradinginfo->items[0]->grades[$USER->id]->hidden ) {
3265 $grade = $gradinginfo->items[0]->grades[$USER->id]->str_grade;
3266 } else {
3267 $grade = '-';
3269 } else if (has_capability('mod/assign:grade', $context)) {
3270 $submitted = $assignment->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED);
3271 $grade = $assignment->count_submissions_need_grading();
3272 $cangrade = true;
3275 $courseindexsummary->add_assign_info($cm->id, $cm->get_formatted_name(),
3276 $sectionname, $timedue, $submitted, $grade, $cangrade);
3279 $o .= $this->get_renderer()->render($courseindexsummary);
3280 $o .= $this->view_footer();
3282 return $o;
3286 * View a page rendered by a plugin.
3288 * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'.
3290 * @return string
3292 protected function view_plugin_page() {
3293 global $USER;
3295 $o = '';
3297 $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
3298 $plugintype = required_param('plugin', PARAM_PLUGIN);
3299 $pluginaction = required_param('pluginaction', PARAM_ALPHA);
3301 $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
3302 if (!$plugin) {
3303 throw new \moodle_exception('invalidformdata', '');
3304 return;
3307 $o .= $plugin->view_page($pluginaction);
3309 return $o;
3314 * This is used for team assignments to get the group for the specified user.
3315 * If the user is a member of multiple or no groups this will return false
3317 * @param int $userid The id of the user whose submission we want
3318 * @return mixed The group or false
3320 public function get_submission_group($userid) {
3322 if (isset($this->usersubmissiongroups[$userid])) {
3323 return $this->usersubmissiongroups[$userid];
3326 $groups = $this->get_all_groups($userid);
3327 if (count($groups) != 1) {
3328 $return = false;
3329 } else {
3330 $return = array_pop($groups);
3333 // Cache the user submission group.
3334 $this->usersubmissiongroups[$userid] = $return;
3336 return $return;
3340 * Gets all groups the user is a member of.
3342 * @param int $userid Teh id of the user who's groups we are checking
3343 * @return array The group objects
3345 public function get_all_groups($userid) {
3346 if (isset($this->usergroups[$userid])) {
3347 return $this->usergroups[$userid];
3350 $grouping = $this->get_instance()->teamsubmissiongroupingid;
3351 $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping, 'g.*', false, true);
3353 $this->usergroups[$userid] = $return;
3355 return $return;
3360 * Display the submission that is used by a plugin.
3362 * Uses url parameters 'sid', 'gid' and 'plugin'.
3364 * @param string $pluginsubtype
3365 * @return string
3367 protected function view_plugin_content($pluginsubtype) {
3368 $o = '';
3370 $submissionid = optional_param('sid', 0, PARAM_INT);
3371 $gradeid = optional_param('gid', 0, PARAM_INT);
3372 $plugintype = required_param('plugin', PARAM_PLUGIN);
3373 $item = null;
3374 if ($pluginsubtype == 'assignsubmission') {
3375 $plugin = $this->get_submission_plugin_by_type($plugintype);
3376 if ($submissionid <= 0) {
3377 throw new coding_exception('Submission id should not be 0');
3379 $item = $this->get_submission($submissionid);
3381 // Check permissions.
3382 if (empty($item->userid)) {
3383 // Group submission.
3384 $this->require_view_group_submission($item->groupid);
3385 } else {
3386 $this->require_view_submission($item->userid);
3388 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3389 $this->get_context(),
3390 $this->show_intro(),
3391 $this->get_course_module()->id,
3392 $plugin->get_name()));
3393 $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin,
3394 $item,
3395 assign_submission_plugin_submission::FULL,
3396 $this->get_course_module()->id,
3397 $this->get_return_action(),
3398 $this->get_return_params()));
3400 // Trigger event for viewing a submission.
3401 \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger();
3403 } else {
3404 $plugin = $this->get_feedback_plugin_by_type($plugintype);
3405 if ($gradeid <= 0) {
3406 throw new coding_exception('Grade id should not be 0');
3408 $item = $this->get_grade($gradeid);
3409 // Check permissions.
3410 $this->require_view_submission($item->userid);
3411 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3412 $this->get_context(),
3413 $this->show_intro(),
3414 $this->get_course_module()->id,
3415 $plugin->get_name()));
3416 $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin,
3417 $item,
3418 assign_feedback_plugin_feedback::FULL,
3419 $this->get_course_module()->id,
3420 $this->get_return_action(),
3421 $this->get_return_params()));
3423 // Trigger event for viewing feedback.
3424 \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger();
3427 $o .= $this->view_return_links();
3429 $o .= $this->view_footer();
3431 return $o;
3435 * Rewrite plugin file urls so they resolve correctly in an exported zip.
3437 * @param string $text - The replacement text
3438 * @param stdClass $user - The user record
3439 * @param assign_plugin $plugin - The assignment plugin
3441 public function download_rewrite_pluginfile_urls($text, $user, $plugin) {
3442 // The groupname prefix for the urls doesn't depend on the group mode of the assignment instance.
3443 // Rather, it should be determined by checking the group submission settings of the instance,
3444 // which is what download_submission() does when generating the file name prefixes.
3445 $groupname = '';
3446 if ($this->get_instance()->teamsubmission) {
3447 $submissiongroup = $this->get_submission_group($user->id);
3448 if ($submissiongroup) {
3449 $groupname = $submissiongroup->name . '-';
3450 } else {
3451 $groupname = get_string('defaultteam', 'assign') . '-';
3455 if ($this->is_blind_marking()) {
3456 $prefix = $groupname . get_string('participant', 'assign');
3457 $prefix = str_replace('_', ' ', $prefix);
3458 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3459 } else {
3460 $prefix = $groupname . fullname($user);
3461 $prefix = str_replace('_', ' ', $prefix);
3462 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_');
3465 // Only prefix files if downloadasfolders user preference is NOT set.
3466 if (!get_user_preferences('assign_downloadasfolders', 1)) {
3467 $subtype = $plugin->get_subtype();
3468 $type = $plugin->get_type();
3469 $prefix = $prefix . $subtype . '_' . $type . '_';
3470 } else {
3471 $prefix = "";
3473 $result = str_replace('@@PLUGINFILE@@/', $prefix, $text);
3475 return $result;
3479 * Render the content in editor that is often used by plugin.
3481 * @param string $filearea
3482 * @param int $submissionid
3483 * @param string $plugintype
3484 * @param string $editor
3485 * @param string $component
3486 * @param bool $shortentext Whether to shorten the text content.
3487 * @return string
3489 public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component, $shortentext = false) {
3490 global $CFG;
3492 $result = '';
3494 $plugin = $this->get_submission_plugin_by_type($plugintype);
3496 $text = $plugin->get_editor_text($editor, $submissionid);
3497 if ($shortentext) {
3498 $text = shorten_text($text, 140);
3500 $format = $plugin->get_editor_format($editor, $submissionid);
3502 $finaltext = file_rewrite_pluginfile_urls($text,
3503 'pluginfile.php',
3504 $this->get_context()->id,
3505 $component,
3506 $filearea,
3507 $submissionid);
3508 $params = array('overflowdiv' => true, 'context' => $this->get_context());
3509 $result .= format_text($finaltext, $format, $params);
3511 if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) {
3512 require_once($CFG->libdir . '/portfoliolib.php');
3514 $button = new portfolio_add_button();
3515 $portfolioparams = array('cmid' => $this->get_course_module()->id,
3516 'sid' => $submissionid,
3517 'plugin' => $plugintype,
3518 'editor' => $editor,
3519 'area'=>$filearea);
3520 $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign');
3521 $fs = get_file_storage();
3523 if ($files = $fs->get_area_files($this->context->id,
3524 $component,
3525 $filearea,
3526 $submissionid,
3527 'timemodified',
3528 false)) {
3529 $button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
3530 } else {
3531 $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
3533 $result .= $button->to_html(PORTFOLIO_ADD_TEXT_LINK);
3535 return $result;
3539 * Display a continue page after grading.
3541 * @param string $message - The message to display.
3542 * @return string
3544 protected function view_savegrading_result($message) {
3545 $o = '';
3546 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3547 $this->get_context(),
3548 $this->show_intro(),
3549 $this->get_course_module()->id,
3550 get_string('savegradingresult', 'assign')));
3551 $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'),
3552 $message,
3553 $this->get_course_module()->id);
3554 $o .= $this->get_renderer()->render($gradingresult);
3555 $o .= $this->view_footer();
3556 return $o;
3559 * Display a continue page after quickgrading.
3561 * @param string $message - The message to display.
3562 * @return string
3564 protected function view_quickgrading_result($message) {
3565 $o = '';
3566 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
3567 $this->get_context(),
3568 $this->show_intro(),
3569 $this->get_course_module()->id,
3570 get_string('quickgradingresult', 'assign')));
3571 $gradingerror = in_array($message, $this->get_error_messages());
3572 $lastpage = optional_param('lastpage', null, PARAM_INT);
3573 $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'),
3574 $message,
3575 $this->get_course_module()->id,
3576 $gradingerror,
3577 $lastpage);
3578 $o .= $this->get_renderer()->render($gradingresult);
3579 $o .= $this->view_footer();
3580 return $o;
3584 * Display the page footer.
3586 * @return string
3588 protected function view_footer() {
3589 // When viewing the footer during PHPUNIT tests a set_state error is thrown.
3590 if (!PHPUNIT_TEST) {
3591 return $this->get_renderer()->render_footer();
3594 return '';
3598 * Throw an error if the permissions to view this users' group submission are missing.
3600 * @param int $groupid Group id.
3601 * @throws required_capability_exception
3603 public function require_view_group_submission($groupid) {
3604 if (!$this->can_view_group_submission($groupid)) {
3605 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3610 * Throw an error if the permissions to view this users submission are missing.
3612 * @throws required_capability_exception
3613 * @return none
3615 public function require_view_submission($userid) {
3616 if (!$this->can_view_submission($userid)) {
3617 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3622 * Throw an error if the permissions to view grades in this assignment are missing.
3624 * @throws required_capability_exception
3625 * @return none
3627 public function require_view_grades() {
3628 if (!$this->can_view_grades()) {
3629 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', '');
3634 * Does this user have view grade or grade permission for this assignment?
3636 * @param mixed $groupid int|null when is set to a value, use this group instead calculating it
3637 * @return bool
3639 public function can_view_grades($groupid = null) {
3640 // Permissions check.
3641 if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
3642 return false;
3644 // Checks for the edge case when user belongs to no groups and groupmode is sep.
3645 if ($this->get_course_module()->effectivegroupmode == SEPARATEGROUPS) {
3646 if ($groupid === null) {
3647 $groupid = groups_get_activity_allowed_groups($this->get_course_module());
3649 $groupflag = has_capability('moodle/site:accessallgroups', $this->get_context());
3650 $groupflag = $groupflag || !empty($groupid);
3651 return (bool)$groupflag;
3653 return true;
3657 * Does this user have grade permission for this assignment?
3659 * @param int|stdClass $user The object or id of the user who will do the editing (default to current user).
3660 * @return bool
3662 public function can_grade($user = null) {
3663 // Permissions check.
3664 if (!has_capability('mod/assign:grade', $this->context, $user)) {
3665 return false;
3668 return true;
3672 * Download a zip file of all assignment submissions.
3674 * @param array|null $userids Array of user ids to download assignment submissions in a zip file
3675 * @return string - If an error occurs, this will contain the error page.
3677 protected function download_submissions($userids = null) {
3678 $downloader = new downloader($this, $userids ?: null);
3679 if ($downloader->load_filelist()) {
3680 $downloader->download_zip();
3682 // Show some notification if we have nothing to download.
3683 $cm = $this->get_course_module();
3684 $renderer = $this->get_renderer();
3685 $header = new assign_header(
3686 $this->get_instance(),
3687 $this->get_context(),
3689 $cm->id,
3690 get_string('downloadall', 'mod_assign')
3692 $result = $renderer->render($header);
3693 $result .= $renderer->notification(get_string('nosubmission', 'mod_assign'));
3694 $url = new moodle_url('/mod/assign/view.php', ['id' => $cm->id, 'action' => 'grading']);
3695 $result .= $renderer->continue_button($url);
3696 $result .= $this->view_footer();
3697 return $result;
3701 * Util function to add a message to the log.
3703 * @deprecated since 2.7 - Use new events system instead.
3704 * (see http://docs.moodle.org/dev/Migrating_logging_calls_in_plugins).
3706 * @param string $action The current action
3707 * @param string $info A detailed description of the change. But no more than 255 characters.
3708 * @param string $url The url to the assign module instance.
3709 * @param bool $return If true, returns the arguments, else adds to log. The purpose of this is to
3710 * retrieve the arguments to use them with the new event system (Event 2).
3711 * @return void|array
3713 public function add_to_log($action = '', $info = '', $url='', $return = false) {
3714 global $USER;
3716 $fullurl = 'view.php?id=' . $this->get_course_module()->id;
3717 if ($url != '') {
3718 $fullurl .= '&' . $url;
3721 $args = array(
3722 $this->get_course()->id,
3723 'assign',
3724 $action,
3725 $fullurl,
3726 $info,
3727 $this->get_course_module()->id
3730 if ($return) {
3731 // We only need to call debugging when returning a value. This is because the call to
3732 // call_user_func_array('add_to_log', $args) will trigger a debugging message of it's own.
3733 debugging('The mod_assign add_to_log() function is now deprecated.', DEBUG_DEVELOPER);
3734 return $args;
3736 call_user_func_array('add_to_log', $args);
3740 * Lazy load the page renderer and expose the renderer to plugins.
3742 * @return assign_renderer
3744 public function get_renderer() {
3745 global $PAGE;
3746 if ($this->output) {
3747 return $this->output;
3749 $this->output = $PAGE->get_renderer('mod_assign', null, RENDERER_TARGET_GENERAL);
3750 return $this->output;
3754 * Load the submission object for a particular user, optionally creating it if required.
3756 * For team assignments there are 2 submissions - the student submission and the team submission
3757 * All files are associated with the team submission but the status of the students contribution is
3758 * recorded separately.
3760 * @param int $userid The id of the user whose submission we want or 0 in which case USER->id is used
3761 * @param bool $create If set to true a new submission object will be created in the database with the status set to "new".
3762 * @param int $attemptnumber - -1 means the latest attempt
3763 * @return stdClass|false The submission
3765 public function get_user_submission($userid, $create, $attemptnumber=-1) {
3766 global $DB, $USER;
3768 if (!$userid) {
3769 $userid = $USER->id;
3771 // If the userid is not null then use userid.
3772 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
3773 if ($attemptnumber >= 0) {
3774 $params['attemptnumber'] = $attemptnumber;
3777 // Only return the row with the highest attemptnumber.
3778 $submission = null;
3779 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1);
3780 if ($submissions) {
3781 $submission = reset($submissions);
3784 if ($submission) {
3785 if ($create) {
3786 $action = optional_param('action', '', PARAM_TEXT);
3787 if ($action == 'editsubmission') {
3788 if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
3789 $submission->timestarted = time();
3790 $DB->update_record('assign_submission', $submission);
3794 return $submission;
3796 if ($create) {
3797 $submission = new stdClass();
3798 $submission->assignment = $this->get_instance()->id;
3799 $submission->userid = $userid;
3800 $submission->timecreated = time();
3801 $submission->timemodified = $submission->timecreated;
3802 $submission->status = ASSIGN_SUBMISSION_STATUS_NEW;
3803 if ($attemptnumber >= 0) {
3804 $submission->attemptnumber = $attemptnumber;
3805 } else {
3806 $submission->attemptnumber = 0;
3808 // Work out if this is the latest submission.
3809 $submission->latest = 0;
3810 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid, 'groupid'=>0);
3811 if ($attemptnumber == -1) {
3812 // This is a new submission so it must be the latest.
3813 $submission->latest = 1;
3814 } else {
3815 // We need to work this out.
3816 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1);
3817 $latestsubmission = null;
3818 if ($result) {
3819 $latestsubmission = reset($result);
3821 if (empty($latestsubmission) || ($attemptnumber > $latestsubmission->attemptnumber)) {
3822 $submission->latest = 1;
3825 $transaction = $DB->start_delegated_transaction();
3826 if ($submission->latest) {
3827 // This is the case when we need to set latest to 0 for all the other attempts.
3828 $DB->set_field('assign_submission', 'latest', 0, $params);
3830 $sid = $DB->insert_record('assign_submission', $submission);
3831 $transaction->allow_commit();
3832 return $DB->get_record('assign_submission', array('id' => $sid));
3834 return false;
3838 * Load the submission object from it's id.
3840 * @param int $submissionid The id of the submission we want
3841 * @return stdClass The submission
3843 protected function get_submission($submissionid) {
3844 global $DB;
3846 $params = array('assignment'=>$this->get_instance()->id, 'id'=>$submissionid);
3847 return $DB->get_record('assign_submission', $params, '*', MUST_EXIST);
3851 * This will retrieve a user flags object from the db optionally creating it if required.
3852 * The user flags was split from the user_grades table in 2.5.
3854 * @param int $userid The user we are getting the flags for.
3855 * @param bool $create If true the flags record will be created if it does not exist
3856 * @return stdClass The flags record
3858 public function get_user_flags($userid, $create) {
3859 global $DB, $USER;
3861 // If the userid is not null then use userid.
3862 if (!$userid) {
3863 $userid = $USER->id;
3866 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
3868 $flags = $DB->get_record('assign_user_flags', $params);
3870 if ($flags) {
3871 return $flags;
3873 if ($create) {
3874 $flags = new stdClass();
3875 $flags->assignment = $this->get_instance()->id;
3876 $flags->userid = $userid;
3877 $flags->locked = 0;
3878 $flags->extensionduedate = 0;
3879 $flags->workflowstate = '';
3880 $flags->allocatedmarker = 0;
3882 // The mailed flag can be one of 3 values: 0 is unsent, 1 is sent and 2 is do not send yet.
3883 // This is because students only want to be notified about certain types of update (grades and feedback).
3884 $flags->mailed = 2;
3886 $fid = $DB->insert_record('assign_user_flags', $flags);
3887 $flags->id = $fid;
3888 return $flags;
3890 return false;
3894 * This will retrieve a grade object from the db, optionally creating it if required.
3896 * @param int $userid The user we are grading
3897 * @param bool $create If true the grade will be created if it does not exist
3898 * @param int $attemptnumber The attempt number to retrieve the grade for. -1 means the latest submission.
3899 * @return stdClass The grade record
3901 public function get_user_grade($userid, $create, $attemptnumber=-1) {
3902 global $DB, $USER;
3904 // If the userid is not null then use userid.
3905 if (!$userid) {
3906 $userid = $USER->id;
3908 $submission = null;
3910 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
3911 if ($attemptnumber < 0 || $create) {
3912 // Make sure this grade matches the latest submission attempt.
3913 if ($this->get_instance()->teamsubmission) {
3914 $submission = $this->get_group_submission($userid, 0, true, $attemptnumber);
3915 } else {
3916 $submission = $this->get_user_submission($userid, true, $attemptnumber);
3918 if ($submission) {
3919 $attemptnumber = $submission->attemptnumber;
3923 if ($attemptnumber >= 0) {
3924 $params['attemptnumber'] = $attemptnumber;
3927 $grades = $DB->get_records('assign_grades', $params, 'attemptnumber DESC', '*', 0, 1);
3929 if ($grades) {
3930 return reset($grades);
3932 if ($create) {
3933 $grade = new stdClass();
3934 $grade->assignment = $this->get_instance()->id;
3935 $grade->userid = $userid;
3936 $grade->timecreated = time();
3937 // If we are "auto-creating" a grade - and there is a submission
3938 // the new grade should not have a more recent timemodified value
3939 // than the submission.
3940 if ($submission) {
3941 $grade->timemodified = $submission->timemodified;
3942 } else {
3943 $grade->timemodified = $grade->timecreated;
3945 $grade->grade = -1;
3946 // Do not set the grader id here as it would be the admin users which is incorrect.
3947 $grade->grader = -1;
3948 if ($attemptnumber >= 0) {
3949 $grade->attemptnumber = $attemptnumber;
3952 $gid = $DB->insert_record('assign_grades', $grade);
3953 $grade->id = $gid;
3954 return $grade;
3956 return false;
3960 * This will retrieve a grade object from the db.
3962 * @param int $gradeid The id of the grade
3963 * @return stdClass The grade record
3965 protected function get_grade($gradeid) {
3966 global $DB;
3968 $params = array('assignment'=>$this->get_instance()->id, 'id'=>$gradeid);
3969 return $DB->get_record('assign_grades', $params, '*', MUST_EXIST);
3973 * Print the grading page for a single user submission.
3975 * @param array $args Optional args array (better than pulling args from _GET and _POST)
3976 * @return string
3978 protected function view_single_grading_panel($args) {
3979 global $DB, $CFG;
3981 $o = '';
3983 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
3985 // Need submit permission to submit an assignment.
3986 require_capability('mod/assign:grade', $this->context);
3988 // If userid is passed - we are only grading a single student.
3989 $userid = $args['userid'];
3990 $attemptnumber = $args['attemptnumber'];
3991 $instance = $this->get_instance($userid);
3993 // Apply overrides.
3994 $this->update_effective_access($userid);
3996 $rownum = 0;
3997 $useridlist = array($userid);
3999 $last = true;
4000 // This variation on the url will link direct to this student, with no next/previous links.
4001 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4002 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4003 $this->register_return_link('grade', $returnparams);
4005 $user = $DB->get_record('user', array('id' => $userid));
4006 $submission = $this->get_user_submission($userid, false, $attemptnumber);
4007 $submissiongroup = null;
4008 $teamsubmission = null;
4009 $notsubmitted = array();
4010 if ($instance->teamsubmission) {
4011 $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
4012 $submissiongroup = $this->get_submission_group($userid);
4013 $groupid = 0;
4014 if ($submissiongroup) {
4015 $groupid = $submissiongroup->id;
4017 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
4021 // Get the requested grade.
4022 $grade = $this->get_user_grade($userid, false, $attemptnumber);
4023 $flags = $this->get_user_flags($userid, false);
4024 if ($this->can_view_submission($userid)) {
4025 $submissionlocked = ($flags && $flags->locked);
4026 $extensionduedate = null;
4027 if ($flags) {
4028 $extensionduedate = $flags->extensionduedate;
4030 $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
4031 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4032 $usergroups = $this->get_all_groups($user->id);
4034 $submissionstatus = new assign_submission_status_compact($instance->allowsubmissionsfromdate,
4035 $instance->alwaysshowdescription,
4036 $submission,
4037 $instance->teamsubmission,
4038 $teamsubmission,
4039 $submissiongroup,
4040 $notsubmitted,
4041 $this->is_any_submission_plugin_enabled(),
4042 $submissionlocked,
4043 $this->is_graded($userid),
4044 $instance->duedate,
4045 $instance->cutoffdate,
4046 $this->get_submission_plugins(),
4047 $this->get_return_action(),
4048 $this->get_return_params(),
4049 $this->get_course_module()->id,
4050 $this->get_course()->id,
4051 assign_submission_status::GRADER_VIEW,
4052 $showedit,
4053 false,
4054 $viewfullnames,
4055 $extensionduedate,
4056 $this->get_context(),
4057 $this->is_blind_marking(),
4059 $instance->attemptreopenmethod,
4060 $instance->maxattempts,
4061 $this->get_grading_status($userid),
4062 $instance->preventsubmissionnotingroup,
4063 $usergroups,
4064 $instance->timelimit);
4065 $o .= $this->get_renderer()->render($submissionstatus);
4068 if ($grade) {
4069 $data = new stdClass();
4070 if ($grade->grade !== null && $grade->grade >= 0) {
4071 $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
4073 } else {
4074 $data = new stdClass();
4075 $data->grade = '';
4078 if (!empty($flags->workflowstate)) {
4079 $data->workflowstate = $flags->workflowstate;
4081 if (!empty($flags->allocatedmarker)) {
4082 $data->allocatedmarker = $flags->allocatedmarker;
4085 // Warning if required.
4086 $allsubmissions = $this->get_all_submissions($userid);
4088 if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
4089 $params = array('attemptnumber' => $attemptnumber + 1,
4090 'totalattempts' => count($allsubmissions));
4091 $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
4092 $o .= $this->get_renderer()->notification($message);
4095 $pagination = array('rownum' => $rownum,
4096 'useridlistid' => 0,
4097 'last' => $last,
4098 'userid' => $userid,
4099 'attemptnumber' => $attemptnumber,
4100 'gradingpanel' => true);
4102 if (!empty($args['formdata'])) {
4103 $data = (array) $data;
4104 $data = (object) array_merge($data, $args['formdata']);
4106 $formparams = array($this, $data, $pagination);
4107 $mform = new mod_assign_grade_form(null,
4108 $formparams,
4109 'post',
4111 array('class' => 'gradeform'));
4113 if (!empty($args['formdata'])) {
4114 // If we were passed form data - we want the form to check the data
4115 // and show errors.
4116 $mform->is_validated();
4118 $o .= $this->get_renderer()->heading(get_string('gradenoun'), 3);
4119 $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
4121 if (count($allsubmissions) > 1) {
4122 $allgrades = $this->get_all_grades($userid);
4123 $history = new assign_attempt_history_chooser($allsubmissions,
4124 $allgrades,
4125 $this->get_course_module()->id,
4126 $userid);
4128 $o .= $this->get_renderer()->render($history);
4131 \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
4133 return $o;
4137 * Print the grading page for a single user submission.
4139 * @param moodleform $mform
4140 * @return string
4142 protected function view_single_grade_page($mform) {
4143 global $DB, $CFG, $SESSION;
4145 $o = '';
4146 $instance = $this->get_instance();
4148 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
4150 // Need submit permission to submit an assignment.
4151 require_capability('mod/assign:grade', $this->context);
4153 $header = new assign_header($instance,
4154 $this->get_context(),
4155 false,
4156 $this->get_course_module()->id,
4157 get_string('grading', 'assign'));
4158 $o .= $this->get_renderer()->render($header);
4160 // If userid is passed - we are only grading a single student.
4161 $rownum = optional_param('rownum', 0, PARAM_INT);
4162 $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
4163 $userid = optional_param('userid', 0, PARAM_INT);
4164 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
4166 if (!$userid) {
4167 $useridlist = $this->get_grading_userid_list(true, $useridlistid);
4168 } else {
4169 $rownum = 0;
4170 $useridlistid = 0;
4171 $useridlist = array($userid);
4174 if ($rownum < 0 || $rownum > count($useridlist)) {
4175 throw new coding_exception('Row is out of bounds for the current grading table: ' . $rownum);
4178 $last = false;
4179 $userid = $useridlist[$rownum];
4180 if ($rownum == count($useridlist) - 1) {
4181 $last = true;
4183 // This variation on the url will link direct to this student, with no next/previous links.
4184 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4185 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4186 $this->register_return_link('grade', $returnparams);
4188 $user = $DB->get_record('user', array('id' => $userid));
4189 if ($user) {
4190 $this->update_effective_access($userid);
4191 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4192 $usersummary = new assign_user_summary($user,
4193 $this->get_course()->id,
4194 $viewfullnames,
4195 $this->is_blind_marking(),
4196 $this->get_uniqueid_for_user($user->id),
4197 // TODO Does not support custom user profile fields (MDL-70456).
4198 \core_user\fields::get_identity_fields($this->get_context(), false),
4199 !$this->is_active_user($userid));
4200 $o .= $this->get_renderer()->render($usersummary);
4202 $submission = $this->get_user_submission($userid, false, $attemptnumber);
4203 $submissiongroup = null;
4204 $teamsubmission = null;
4205 $notsubmitted = array();
4206 if ($instance->teamsubmission) {
4207 $teamsubmission = $this->get_group_submission($userid, 0, false, $attemptnumber);
4208 $submissiongroup = $this->get_submission_group($userid);
4209 $groupid = 0;
4210 if ($submissiongroup) {
4211 $groupid = $submissiongroup->id;
4213 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
4217 // Get the requested grade.
4218 $grade = $this->get_user_grade($userid, false, $attemptnumber);
4219 $flags = $this->get_user_flags($userid, false);
4220 if ($this->can_view_submission($userid)) {
4221 $submissionlocked = ($flags && $flags->locked);
4222 $extensionduedate = null;
4223 if ($flags) {
4224 $extensionduedate = $flags->extensionduedate;
4226 $showedit = $this->submissions_open($userid) && ($this->is_any_submission_plugin_enabled());
4227 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
4228 $usergroups = $this->get_all_groups($user->id);
4229 $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
4230 $instance->alwaysshowdescription,
4231 $submission,
4232 $instance->teamsubmission,
4233 $teamsubmission,
4234 $submissiongroup,
4235 $notsubmitted,
4236 $this->is_any_submission_plugin_enabled(),
4237 $submissionlocked,
4238 $this->is_graded($userid),
4239 $instance->duedate,
4240 $instance->cutoffdate,
4241 $this->get_submission_plugins(),
4242 $this->get_return_action(),
4243 $this->get_return_params(),
4244 $this->get_course_module()->id,
4245 $this->get_course()->id,
4246 assign_submission_status::GRADER_VIEW,
4247 $showedit,
4248 false,
4249 $viewfullnames,
4250 $extensionduedate,
4251 $this->get_context(),
4252 $this->is_blind_marking(),
4254 $instance->attemptreopenmethod,
4255 $instance->maxattempts,
4256 $this->get_grading_status($userid),
4257 $instance->preventsubmissionnotingroup,
4258 $usergroups,
4259 $instance->timelimit);
4260 $o .= $this->get_renderer()->render($submissionstatus);
4263 if ($grade) {
4264 $data = new stdClass();
4265 if ($grade->grade !== null && $grade->grade >= 0) {
4266 $data->grade = format_float($grade->grade, $this->get_grade_item()->get_decimals());
4268 } else {
4269 $data = new stdClass();
4270 $data->grade = '';
4273 if (!empty($flags->workflowstate)) {
4274 $data->workflowstate = $flags->workflowstate;
4276 if (!empty($flags->allocatedmarker)) {
4277 $data->allocatedmarker = $flags->allocatedmarker;
4280 // Warning if required.
4281 $allsubmissions = $this->get_all_submissions($userid);
4283 if ($attemptnumber != -1 && ($attemptnumber + 1) != count($allsubmissions)) {
4284 $params = array('attemptnumber'=>$attemptnumber + 1,
4285 'totalattempts'=>count($allsubmissions));
4286 $message = get_string('editingpreviousfeedbackwarning', 'assign', $params);
4287 $o .= $this->get_renderer()->notification($message);
4290 // Now show the grading form.
4291 if (!$mform) {
4292 $pagination = array('rownum' => $rownum,
4293 'useridlistid' => $useridlistid,
4294 'last' => $last,
4295 'userid' => $userid,
4296 'attemptnumber' => $attemptnumber);
4297 $formparams = array($this, $data, $pagination);
4298 $mform = new mod_assign_grade_form(null,
4299 $formparams,
4300 'post',
4302 array('class'=>'gradeform'));
4304 $o .= $this->get_renderer()->heading(get_string('gradenoun'), 3);
4305 $o .= $this->get_renderer()->render(new assign_form('gradingform', $mform));
4307 if (count($allsubmissions) > 1 && $attemptnumber == -1) {
4308 $allgrades = $this->get_all_grades($userid);
4309 $history = new assign_attempt_history($allsubmissions,
4310 $allgrades,
4311 $this->get_submission_plugins(),
4312 $this->get_feedback_plugins(),
4313 $this->get_course_module()->id,
4314 $this->get_return_action(),
4315 $this->get_return_params(),
4316 true,
4317 $useridlistid,
4318 $rownum);
4320 $o .= $this->get_renderer()->render($history);
4323 \mod_assign\event\grading_form_viewed::create_from_user($this, $user)->trigger();
4325 $o .= $this->view_footer();
4326 return $o;
4330 * Show a confirmation page to make sure they want to remove submission data.
4332 * @return string
4334 protected function view_remove_submission_confirm() {
4335 global $USER;
4337 $userid = optional_param('userid', $USER->id, PARAM_INT);
4339 if (!$this->can_edit_submission($userid, $USER->id)) {
4340 throw new \moodle_exception('nopermission');
4342 $user = core_user::get_user($userid, '*', MUST_EXIST);
4344 $o = '';
4345 $header = new assign_header($this->get_instance(),
4346 $this->get_context(),
4347 false,
4348 $this->get_course_module()->id);
4349 $o .= $this->get_renderer()->render($header);
4351 $urlparams = array('id' => $this->get_course_module()->id,
4352 'action' => 'removesubmission',
4353 'userid' => $userid,
4354 'sesskey' => sesskey());
4355 $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
4357 $urlparams = array('id' => $this->get_course_module()->id,
4358 'action' => 'view');
4359 $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
4361 if ($userid == $USER->id) {
4362 if ($this->is_time_limit_enabled($userid)) {
4363 $confirmstr = get_string('removesubmissionconfirmwithtimelimit', 'assign');
4364 } else {
4365 $confirmstr = get_string('removesubmissionconfirm', 'assign');
4367 } else {
4368 if ($this->is_time_limit_enabled($userid)) {
4369 $confirmstr = get_string('removesubmissionconfirmforstudentwithtimelimit', 'assign', $this->fullname($user));
4370 } else {
4371 $confirmstr = get_string('removesubmissionconfirmforstudent', 'assign', $this->fullname($user));
4374 $o .= $this->get_renderer()->confirm($confirmstr,
4375 $confirmurl,
4376 $cancelurl);
4377 $o .= $this->view_footer();
4379 \mod_assign\event\remove_submission_form_viewed::create_from_user($this, $user)->trigger();
4381 return $o;
4386 * Show a confirmation page to make sure they want to release student identities.
4388 * @return string
4390 protected function view_reveal_identities_confirm() {
4391 require_capability('mod/assign:revealidentities', $this->get_context());
4393 $o = '';
4394 $header = new assign_header($this->get_instance(),
4395 $this->get_context(),
4396 false,
4397 $this->get_course_module()->id);
4398 $o .= $this->get_renderer()->render($header);
4400 $urlparams = array('id'=>$this->get_course_module()->id,
4401 'action'=>'revealidentitiesconfirm',
4402 'sesskey'=>sesskey());
4403 $confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
4405 $urlparams = array('id'=>$this->get_course_module()->id,
4406 'action'=>'grading');
4407 $cancelurl = new moodle_url('/mod/assign/view.php', $urlparams);
4409 $o .= $this->get_renderer()->confirm(get_string('revealidentitiesconfirm', 'assign'),
4410 $confirmurl,
4411 $cancelurl);
4412 $o .= $this->view_footer();
4414 \mod_assign\event\reveal_identities_confirmation_page_viewed::create_from_assign($this)->trigger();
4416 return $o;
4420 * View a link to go back to the previous page. Uses url parameters returnaction and returnparams.
4422 * @return string
4424 protected function view_return_links() {
4425 $returnaction = optional_param('returnaction', '', PARAM_ALPHA);
4426 $returnparams = optional_param('returnparams', '', PARAM_TEXT);
4428 $params = array();
4429 $returnparams = str_replace('&amp;', '&', $returnparams);
4430 parse_str($returnparams, $params);
4431 $newparams = array('id' => $this->get_course_module()->id, 'action' => $returnaction);
4432 $params = array_merge($newparams, $params);
4434 $url = new moodle_url('/mod/assign/view.php', $params);
4435 return $this->get_renderer()->single_button($url, get_string('back'), 'get');
4439 * View the grading table of all submissions for this assignment.
4441 * @return string
4443 protected function view_grading_table() {
4444 global $USER, $CFG, $SESSION, $PAGE;
4446 // Include grading options form.
4447 require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
4448 require_once($CFG->dirroot . '/mod/assign/quickgradingform.php');
4449 require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
4450 $o = '';
4451 $cmid = $this->get_course_module()->id;
4453 $links = array();
4454 if (has_capability('gradereport/grader:view', $this->get_course_context()) &&
4455 has_capability('moodle/grade:viewall', $this->get_course_context())) {
4456 $gradebookurl = '/grade/report/grader/index.php?id=' . $this->get_course()->id;
4457 $links[$gradebookurl] = get_string('viewgradebook', 'assign');
4459 if ($this->is_blind_marking() &&
4460 has_capability('mod/assign:revealidentities', $this->get_context())) {
4461 $revealidentitiesurl = '/mod/assign/view.php?id=' . $cmid . '&action=revealidentities';
4462 $links[$revealidentitiesurl] = get_string('revealidentities', 'assign');
4464 foreach ($this->get_feedback_plugins() as $plugin) {
4465 if ($plugin->is_enabled() && $plugin->is_visible()) {
4466 foreach ($plugin->get_grading_actions() as $action => $description) {
4467 $url = '/mod/assign/view.php' .
4468 '?id=' . $cmid .
4469 '&plugin=' . $plugin->get_type() .
4470 '&pluginsubtype=assignfeedback' .
4471 '&action=viewpluginpage&pluginaction=' . $action;
4472 $links[$url] = $description;
4477 // Sort links alphabetically based on the link description.
4478 core_collator::asort($links);
4480 $gradingactions = new url_select($links);
4481 $gradingactions->set_label(get_string('choosegradingaction', 'assign'));
4482 $gradingactions->class .= ' mb-1';
4484 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
4486 $perpage = $this->get_assign_perpage();
4487 $filter = get_user_preferences('assign_filter', '');
4488 $markerfilter = get_user_preferences('assign_markerfilter', '');
4489 $workflowfilter = get_user_preferences('assign_workflowfilter', '');
4490 $controller = $gradingmanager->get_active_controller();
4491 $showquickgrading = empty($controller) && $this->can_grade();
4492 $quickgrading = get_user_preferences('assign_quickgrading', false);
4493 $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
4494 $downloadasfolders = get_user_preferences('assign_downloadasfolders', 1);
4496 $markingallocation = $this->get_instance()->markingworkflow &&
4497 $this->get_instance()->markingallocation &&
4498 has_capability('mod/assign:manageallocations', $this->context);
4499 // Get markers to use in drop lists.
4500 $markingallocationoptions = array();
4501 if ($markingallocation) {
4502 list($sort, $params) = users_order_by_sql('u');
4503 // Only enrolled users could be assigned as potential markers.
4504 $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
4505 $markingallocationoptions[''] = get_string('filternone', 'assign');
4506 $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
4507 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
4508 foreach ($markers as $marker) {
4509 $markingallocationoptions[$marker->id] = fullname($marker, $viewfullnames);
4513 $markingworkflow = $this->get_instance()->markingworkflow;
4514 // Get marking states to show in form.
4515 $markingworkflowoptions = $this->get_marking_workflow_filters();
4517 // Print options for changing the filter and changing the number of results per page.
4518 $gradingoptionsformparams = array('cm'=>$cmid,
4519 'contextid'=>$this->context->id,
4520 'userid'=>$USER->id,
4521 'submissionsenabled'=>$this->is_any_submission_plugin_enabled(),
4522 'showquickgrading'=>$showquickgrading,
4523 'quickgrading'=>$quickgrading,
4524 'markingworkflowopt'=>$markingworkflowoptions,
4525 'markingallocationopt'=>$markingallocationoptions,
4526 'showonlyactiveenrolopt'=>$showonlyactiveenrolopt,
4527 'showonlyactiveenrol' => $this->show_only_active_users(),
4528 'downloadasfolders' => $downloadasfolders);
4530 $classoptions = array('class'=>'gradingoptionsform');
4531 $gradingoptionsform = new mod_assign_grading_options_form(null,
4532 $gradingoptionsformparams,
4533 'post',
4535 $classoptions);
4537 $batchformparams = array('cm'=>$cmid,
4538 'submissiondrafts'=>$this->get_instance()->submissiondrafts,
4539 'duedate'=>$this->get_instance()->duedate,
4540 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
4541 'feedbackplugins'=>$this->get_feedback_plugins(),
4542 'context'=>$this->get_context(),
4543 'markingworkflow'=>$markingworkflow,
4544 'markingallocation'=>$markingallocation);
4545 $classoptions = array('class'=>'gradingbatchoperationsform');
4547 $gradingbatchoperationsform = new mod_assign_grading_batch_operations_form(null,
4548 $batchformparams,
4549 'post',
4551 $classoptions);
4553 $gradingoptionsdata = new stdClass();
4554 $gradingoptionsdata->perpage = $perpage;
4555 $gradingoptionsdata->filter = $filter;
4556 $gradingoptionsdata->markerfilter = $markerfilter;
4557 $gradingoptionsdata->workflowfilter = $workflowfilter;
4558 $gradingoptionsform->set_data($gradingoptionsdata);
4560 $buttons = new \mod_assign\output\grading_actionmenu($this->get_course_module()->id,
4561 $this->is_any_submission_plugin_enabled(), $this->count_submissions());
4562 $actionformtext = $this->get_renderer()->render($buttons);
4563 $PAGE->activityheader->set_attrs(['hidecompletion' => true]);
4565 $currenturl = new moodle_url('/mod/assign/view.php', ['id' => $this->get_course_module()->id, 'action' => 'grading']);
4567 $header = new assign_header($this->get_instance(),
4568 $this->get_context(),
4569 false,
4570 $this->get_course_module()->id,
4571 get_string('grading', 'assign'),
4574 $currenturl);
4575 $o .= $this->get_renderer()->render($header);
4577 $o .= $actionformtext;
4579 $o .= $this->get_renderer()->heading(get_string('gradeitem:submissions', 'mod_assign'), 2);
4580 $o .= $this->get_renderer()->render($gradingactions);
4582 $o .= groups_print_activity_menu($this->get_course_module(), $currenturl, true);
4584 // Plagiarism update status apearring in the grading book.
4585 if (!empty($CFG->enableplagiarism)) {
4586 require_once($CFG->libdir . '/plagiarismlib.php');
4587 $o .= plagiarism_update_status($this->get_course(), $this->get_course_module());
4590 if ($this->is_blind_marking() && has_capability('mod/assign:viewblinddetails', $this->get_context())) {
4591 $o .= $this->get_renderer()->notification(get_string('blindmarkingenabledwarning', 'assign'), 'notifymessage');
4594 // Load and print the table of submissions.
4595 if ($showquickgrading && $quickgrading) {
4596 $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, true);
4597 $table = $this->get_renderer()->render($gradingtable);
4598 $page = optional_param('page', null, PARAM_INT);
4599 $quickformparams = array('cm'=>$this->get_course_module()->id,
4600 'gradingtable'=>$table,
4601 'sendstudentnotifications' => $this->get_instance()->sendstudentnotifications,
4602 'page' => $page);
4603 $quickgradingform = new mod_assign_quick_grading_form(null, $quickformparams);
4605 $o .= $this->get_renderer()->render(new assign_form('quickgradingform', $quickgradingform));
4606 } else {
4607 $gradingtable = new assign_grading_table($this, $perpage, $filter, 0, false);
4608 $o .= $this->get_renderer()->render($gradingtable);
4611 if ($this->can_grade()) {
4612 // We need to store the order of uses in the table as the person may wish to grade them.
4613 // This is done based on the row number of the user.
4614 $useridlist = $gradingtable->get_column_data('userid');
4615 $SESSION->mod_assign_useridlist[$this->get_useridlist_key()] = $useridlist;
4618 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4619 $users = array_keys($this->list_participants($currentgroup, true));
4620 if (count($users) != 0 && $this->can_grade()) {
4621 // If no enrolled user in a course then don't display the batch operations feature.
4622 $assignform = new assign_form('gradingbatchoperationsform', $gradingbatchoperationsform);
4623 $o .= $this->get_renderer()->render($assignform);
4625 $assignform = new assign_form('gradingoptionsform',
4626 $gradingoptionsform,
4627 'M.mod_assign.init_grading_options');
4628 $o .= $this->get_renderer()->render($assignform);
4629 return $o;
4633 * View entire grader app.
4635 * @return string
4637 protected function view_grader() {
4638 global $USER, $PAGE;
4640 $o = '';
4641 // Need submit permission to submit an assignment.
4642 $this->require_view_grades();
4644 $PAGE->set_pagelayout('embedded');
4646 $PAGE->activityheader->disable();
4648 $courseshortname = $this->get_context()->get_course_context()->get_context_name(false, true);
4649 $args = [
4650 'contextname' => $this->get_context()->get_context_name(false, true),
4651 'subpage' => get_string('grading', 'assign')
4653 $title = get_string('subpagetitle', 'assign', $args);
4654 $title = $courseshortname . ': ' . $title;
4655 $PAGE->set_title($title);
4657 $o .= $this->get_renderer()->header();
4659 $userid = optional_param('userid', 0, PARAM_INT);
4660 $blindid = optional_param('blindid', 0, PARAM_INT);
4662 if (!$userid && $blindid) {
4663 $userid = $this->get_user_id_for_uniqueid($blindid);
4666 // Instantiate table object to apply table preferences.
4667 $gradingtable = new assign_grading_table($this, 10, '', 0, false);
4668 $gradingtable->setup();
4670 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
4671 $framegrader = new grading_app($userid, $currentgroup, $this);
4673 $this->update_effective_access($userid);
4675 $o .= $this->get_renderer()->render($framegrader);
4677 $o .= $this->view_footer();
4679 \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4681 return $o;
4684 * View entire grading page.
4686 * @return string
4688 protected function view_grading_page() {
4689 global $CFG;
4691 $o = '';
4692 // Need submit permission to submit an assignment.
4693 $this->require_view_grades();
4694 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
4696 $this->add_grade_notices();
4698 // Only load this if it is.
4699 $o .= $this->view_grading_table();
4701 $o .= $this->view_footer();
4703 \mod_assign\event\grading_table_viewed::create_from_assign($this)->trigger();
4705 return $o;
4709 * Capture the output of the plagiarism plugins disclosures and return it as a string.
4711 * @return string
4713 protected function plagiarism_print_disclosure() {
4714 global $CFG;
4715 $o = '';
4717 if (!empty($CFG->enableplagiarism)) {
4718 require_once($CFG->libdir . '/plagiarismlib.php');
4720 $o .= plagiarism_print_disclosure($this->get_course_module()->id);
4723 return $o;
4727 * Message for students when assignment submissions have been closed.
4729 * @param string $title The page title
4730 * @param array $notices The array of notices to show.
4731 * @return string
4733 protected function view_notices($title, $notices) {
4734 global $CFG;
4736 $o = '';
4738 $header = new assign_header($this->get_instance(),
4739 $this->get_context(),
4740 $this->show_intro(),
4741 $this->get_course_module()->id,
4742 $title);
4743 $o .= $this->get_renderer()->render($header);
4745 foreach ($notices as $notice) {
4746 $o .= $this->get_renderer()->notification($notice);
4749 $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id, 'action'=>'view'));
4750 $o .= $this->get_renderer()->continue_button($url);
4752 $o .= $this->view_footer();
4754 return $o;
4758 * Get the name for a user - hiding their real name if blind marking is on.
4760 * @param stdClass $user The user record as required by fullname()
4761 * @return string The name.
4763 public function fullname($user) {
4764 if ($this->is_blind_marking()) {
4765 $hasviewblind = has_capability('mod/assign:viewblinddetails', $this->get_context());
4766 if (empty($user->recordid)) {
4767 $uniqueid = $this->get_uniqueid_for_user($user->id);
4768 } else {
4769 $uniqueid = $user->recordid;
4771 if ($hasviewblind) {
4772 return get_string('participant', 'assign') . ' ' . $uniqueid . ' (' .
4773 fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())) . ')';
4774 } else {
4775 return get_string('participant', 'assign') . ' ' . $uniqueid;
4777 } else {
4778 return fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context()));
4783 * View edit submissions page.
4785 * @param moodleform $mform
4786 * @param array $notices A list of notices to display at the top of the
4787 * edit submission form (e.g. from plugins).
4788 * @return string The page output.
4790 protected function view_edit_submission_page($mform, $notices) {
4791 global $CFG, $USER, $DB, $PAGE;
4793 $o = '';
4794 require_once($CFG->dirroot . '/mod/assign/submission_form.php');
4795 // Need submit permission to submit an assignment.
4796 $userid = optional_param('userid', $USER->id, PARAM_INT);
4797 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
4798 $timelimitenabled = get_config('assign', 'enabletimelimit');
4800 // This variation on the url will link direct to this student.
4801 // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
4802 $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
4803 $this->register_return_link('editsubmission', $returnparams);
4805 if ($userid == $USER->id) {
4806 if (!$this->can_edit_submission($userid, $USER->id)) {
4807 throw new \moodle_exception('nopermission');
4809 // User is editing their own submission.
4810 require_capability('mod/assign:submit', $this->context);
4811 $title = get_string('editsubmission', 'assign');
4812 } else {
4813 // User is editing another user's submission.
4814 if (!$this->can_edit_submission($userid, $USER->id)) {
4815 throw new \moodle_exception('nopermission');
4818 $name = $this->fullname($user);
4819 $title = get_string('editsubmissionother', 'assign', $name);
4822 if (!$this->submissions_open($userid)) {
4823 $message = array(get_string('submissionsclosed', 'assign'));
4824 return $this->view_notices($title, $message);
4827 $postfix = '';
4828 if ($this->has_visible_attachments()) {
4829 $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
4832 $data = new stdClass();
4833 $data->userid = $userid;
4834 if (!$mform) {
4835 $mform = new mod_assign_submission_form(null, array($this, $data));
4838 if ($this->get_instance()->teamsubmission) {
4839 $submission = $this->get_group_submission($userid, 0, false);
4840 } else {
4841 $submission = $this->get_user_submission($userid, false);
4844 if ($timelimitenabled && !empty($submission->timestarted) && $this->get_instance()->timelimit) {
4845 $navbc = $this->get_timelimit_panel($submission);
4846 $regions = $PAGE->blocks->get_regions();
4847 $bc = new \block_contents();
4848 $bc->attributes['id'] = 'mod_assign_timelimit_block';
4849 $bc->attributes['role'] = 'navigation';
4850 $bc->attributes['aria-labelledby'] = 'mod_assign_timelimit_block_title';
4851 $bc->title = get_string('assigntimeleft', 'assign');
4852 $bc->content = $navbc;
4853 $PAGE->blocks->add_fake_block($bc, reset($regions));
4856 $o .= $this->get_renderer()->render(
4857 new assign_header($this->get_instance(),
4858 $this->get_context(),
4859 $this->show_intro(),
4860 $this->get_course_module()->id,
4861 $title,
4863 $postfix,
4864 null,
4865 true
4869 // Show plagiarism disclosure for any user submitter.
4870 $o .= $this->plagiarism_print_disclosure();
4872 foreach ($notices as $notice) {
4873 $o .= $this->get_renderer()->notification($notice);
4876 $o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
4877 $o .= $this->view_footer();
4879 \mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger();
4881 return $o;
4885 * Get the time limit panel object for this submission attempt.
4887 * @param stdClass $submission assign submission.
4888 * @return string the panel output.
4890 public function get_timelimit_panel(stdClass $submission): string {
4891 global $USER;
4893 // Apply overrides.
4894 $this->update_effective_access($USER->id);
4895 $panel = new timelimit_panel($submission, $this->get_instance());
4896 return $this->get_renderer()->render($panel);
4900 * See if this assignment has a grade yet.
4902 * @param int $userid
4903 * @return bool
4905 protected function is_graded($userid) {
4906 $grade = $this->get_user_grade($userid, false);
4907 if ($grade) {
4908 return ($grade->grade !== null && $grade->grade >= 0);
4910 return false;
4914 * Perform an access check to see if the current $USER can edit this group submission.
4916 * @param int $groupid
4917 * @return bool
4919 public function can_edit_group_submission($groupid) {
4920 global $USER;
4922 $members = $this->get_submission_group_members($groupid, true);
4923 foreach ($members as $member) {
4924 // If we can edit any members submission, we can edit the submission for the group.
4925 if ($this->can_edit_submission($member->id)) {
4926 return true;
4929 return false;
4933 * Perform an access check to see if the current $USER can view this group submission.
4935 * @param int $groupid
4936 * @return bool
4938 public function can_view_group_submission($groupid) {
4939 global $USER;
4941 $members = $this->get_submission_group_members($groupid, true);
4942 foreach ($members as $member) {
4943 // If we can view any members submission, we can view the submission for the group.
4944 if ($this->can_view_submission($member->id)) {
4945 return true;
4948 return false;
4952 * Perform an access check to see if the current $USER can view this users submission.
4954 * @param int $userid
4955 * @return bool
4957 public function can_view_submission($userid) {
4958 global $USER;
4960 if (!$this->is_active_user($userid) && !has_capability('moodle/course:viewsuspendedusers', $this->context)) {
4961 return false;
4963 if (!is_enrolled($this->get_course_context(), $userid)) {
4964 return false;
4966 if (has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) {
4967 return true;
4969 if ($userid == $USER->id) {
4970 return true;
4972 return false;
4976 * Allows the plugin to show a batch grading operation page.
4978 * @param moodleform $mform
4979 * @return none
4981 protected function view_plugin_grading_batch_operation($mform) {
4982 require_capability('mod/assign:grade', $this->context);
4983 $prefix = 'plugingradingbatchoperation_';
4985 if ($data = $mform->get_data()) {
4986 $tail = substr($data->operation, strlen($prefix));
4987 list($plugintype, $action) = explode('_', $tail, 2);
4989 $plugin = $this->get_feedback_plugin_by_type($plugintype);
4990 if ($plugin) {
4991 $users = $data->selectedusers;
4992 $userlist = explode(',', $users);
4993 echo $plugin->grading_batch_operation($action, $userlist);
4994 return;
4997 throw new \moodle_exception('invalidformdata', '');
5001 * Ask the user to confirm they want to perform this batch operation
5003 * @param moodleform $mform Set to a grading batch operations form
5004 * @return string - the page to view after processing these actions
5006 protected function process_grading_batch_operation(& $mform) {
5007 global $CFG;
5008 require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
5009 require_sesskey();
5011 $markingallocation = $this->get_instance()->markingworkflow &&
5012 $this->get_instance()->markingallocation &&
5013 has_capability('mod/assign:manageallocations', $this->context);
5015 $batchformparams = array('cm'=>$this->get_course_module()->id,
5016 'submissiondrafts'=>$this->get_instance()->submissiondrafts,
5017 'duedate'=>$this->get_instance()->duedate,
5018 'attemptreopenmethod'=>$this->get_instance()->attemptreopenmethod,
5019 'feedbackplugins'=>$this->get_feedback_plugins(),
5020 'context'=>$this->get_context(),
5021 'markingworkflow'=>$this->get_instance()->markingworkflow,
5022 'markingallocation'=>$markingallocation);
5023 $formclasses = array('class'=>'gradingbatchoperationsform');
5024 $mform = new mod_assign_grading_batch_operations_form(null,
5025 $batchformparams,
5026 'post',
5028 $formclasses);
5030 if ($data = $mform->get_data()) {
5031 // Get the list of users.
5032 $users = $data->selectedusers;
5033 $userlist = explode(',', $users);
5035 $prefix = 'plugingradingbatchoperation_';
5037 if ($data->operation == 'grantextension') {
5038 // Reset the form so the grant extension page will create the extension form.
5039 $mform = null;
5040 return 'grantextension';
5041 } else if ($data->operation == 'setmarkingworkflowstate') {
5042 return 'viewbatchsetmarkingworkflowstate';
5043 } else if ($data->operation == 'setmarkingallocation') {
5044 return 'viewbatchmarkingallocation';
5045 } else if (strpos($data->operation, $prefix) === 0) {
5046 $tail = substr($data->operation, strlen($prefix));
5047 list($plugintype, $action) = explode('_', $tail, 2);
5049 $plugin = $this->get_feedback_plugin_by_type($plugintype);
5050 if ($plugin) {
5051 return 'plugingradingbatchoperation';
5055 if ($data->operation == 'downloadselected') {
5056 $this->download_submissions($userlist);
5057 } else {
5058 foreach ($userlist as $userid) {
5059 if ($data->operation == 'lock') {
5060 $this->process_lock_submission($userid);
5061 } else if ($data->operation == 'unlock') {
5062 $this->process_unlock_submission($userid);
5063 } else if ($data->operation == 'reverttodraft') {
5064 $this->process_revert_to_draft($userid);
5065 } else if ($data->operation == 'removesubmission') {
5066 $this->process_remove_submission($userid);
5067 } else if ($data->operation == 'addattempt') {
5068 if (!$this->get_instance()->teamsubmission) {
5069 $this->process_add_attempt($userid);
5074 if ($this->get_instance()->teamsubmission && $data->operation == 'addattempt') {
5075 // This needs to be handled separately so that each team submission is only re-opened one time.
5076 $this->process_add_attempt_group($userlist);
5080 return 'grading';
5084 * Shows a form that allows the workflow state for selected submissions to be changed.
5086 * @param moodleform $mform Set to a grading batch operations form
5087 * @return string - the page to view after processing these actions
5089 protected function view_batch_set_workflow_state($mform) {
5090 global $CFG, $DB;
5092 require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
5094 $o = '';
5096 $submitteddata = $mform->get_data();
5097 $users = $submitteddata->selectedusers;
5098 $userlist = explode(',', $users);
5100 $formdata = array('id' => $this->get_course_module()->id,
5101 'selectedusers' => $users);
5103 $usershtml = '';
5105 $usercount = 0;
5106 // TODO Does not support custom user profile fields (MDL-70456).
5107 $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false);
5108 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5109 foreach ($userlist as $userid) {
5110 if ($usercount >= 5) {
5111 $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
5112 break;
5114 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
5116 $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
5117 $this->get_course()->id,
5118 $viewfullnames,
5119 $this->is_blind_marking(),
5120 $this->get_uniqueid_for_user($user->id),
5121 $extrauserfields,
5122 !$this->is_active_user($userid)));
5123 $usercount += 1;
5126 $formparams = array(
5127 'userscount' => count($userlist),
5128 'usershtml' => $usershtml,
5129 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
5132 $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
5133 $mform->set_data($formdata); // Initialises the hidden elements.
5134 $header = new assign_header($this->get_instance(),
5135 $this->get_context(),
5136 $this->show_intro(),
5137 $this->get_course_module()->id,
5138 get_string('setmarkingworkflowstate', 'assign'));
5139 $o .= $this->get_renderer()->render($header);
5140 $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
5141 $o .= $this->view_footer();
5143 \mod_assign\event\batch_set_workflow_state_viewed::create_from_assign($this)->trigger();
5145 return $o;
5149 * Shows a form that allows the allocated marker for selected submissions to be changed.
5151 * @param moodleform $mform Set to a grading batch operations form
5152 * @return string - the page to view after processing these actions
5154 public function view_batch_markingallocation($mform) {
5155 global $CFG, $DB;
5157 require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
5159 $o = '';
5161 $submitteddata = $mform->get_data();
5162 $users = $submitteddata->selectedusers;
5163 $userlist = explode(',', $users);
5165 $formdata = array('id' => $this->get_course_module()->id,
5166 'selectedusers' => $users);
5168 $usershtml = '';
5170 $usercount = 0;
5171 // TODO Does not support custom user profile fields (MDL-70456).
5172 $extrauserfields = \core_user\fields::get_identity_fields($this->get_context(), false);
5173 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5174 foreach ($userlist as $userid) {
5175 if ($usercount >= 5) {
5176 $usershtml .= get_string('moreusers', 'assign', count($userlist) - 5);
5177 break;
5179 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
5181 $usershtml .= $this->get_renderer()->render(new assign_user_summary($user,
5182 $this->get_course()->id,
5183 $viewfullnames,
5184 $this->is_blind_marking(),
5185 $this->get_uniqueid_for_user($user->id),
5186 $extrauserfields,
5187 !$this->is_active_user($userid)));
5188 $usercount += 1;
5191 $formparams = array(
5192 'userscount' => count($userlist),
5193 'usershtml' => $usershtml,
5196 list($sort, $params) = users_order_by_sql('u');
5197 // Only enrolled users could be assigned as potential markers.
5198 $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
5199 $markerlist = array();
5200 foreach ($markers as $marker) {
5201 $markerlist[$marker->id] = fullname($marker);
5204 $formparams['markers'] = $markerlist;
5206 $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
5207 $mform->set_data($formdata); // Initialises the hidden elements.
5208 $header = new assign_header($this->get_instance(),
5209 $this->get_context(),
5210 $this->show_intro(),
5211 $this->get_course_module()->id,
5212 get_string('setmarkingallocation', 'assign'));
5213 $o .= $this->get_renderer()->render($header);
5214 $o .= $this->get_renderer()->render(new assign_form('setworkflowstate', $mform));
5215 $o .= $this->view_footer();
5217 \mod_assign\event\batch_set_marker_allocation_viewed::create_from_assign($this)->trigger();
5219 return $o;
5223 * Ask the user to confirm they want to submit their work for grading.
5225 * @param moodleform $mform - null unless form validation has failed
5226 * @return string
5228 protected function check_submit_for_grading($mform) {
5229 global $USER, $CFG;
5231 require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
5233 // Check that all of the submission plugins are ready for this submission.
5234 // Also check whether there is something to be submitted as well against atleast one.
5235 $notifications = array();
5236 $submission = $this->get_user_submission($USER->id, false);
5237 if ($this->get_instance()->teamsubmission) {
5238 $submission = $this->get_group_submission($USER->id, 0, false);
5241 $plugins = $this->get_submission_plugins();
5242 $hassubmission = false;
5243 foreach ($plugins as $plugin) {
5244 if ($plugin->is_enabled() && $plugin->is_visible()) {
5245 $check = $plugin->precheck_submission($submission);
5246 if ($check !== true) {
5247 $notifications[] = $check;
5250 if (is_object($submission) && !$plugin->is_empty($submission)) {
5251 $hassubmission = true;
5256 // If there are no submissions and no existing notifications to be displayed the stop.
5257 if (!$hassubmission && !$notifications) {
5258 $notifications[] = get_string('addsubmission_help', 'assign');
5261 $data = new stdClass();
5262 $adminconfig = $this->get_admin_config();
5263 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
5264 $submissionstatement = '';
5266 if ($requiresubmissionstatement) {
5267 $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
5270 // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
5271 // that the submission statement checkbox will be displayed.
5272 if (empty($submissionstatement)) {
5273 $requiresubmissionstatement = false;
5276 if ($mform == null) {
5277 $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
5278 $submissionstatement,
5279 $this->get_course_module()->id,
5280 $data));
5282 $o = '';
5283 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
5284 $this->get_context(),
5285 $this->show_intro(),
5286 $this->get_course_module()->id,
5287 get_string('confirmsubmissionheading', 'assign')));
5288 $submitforgradingpage = new assign_submit_for_grading_page($notifications,
5289 $this->get_course_module()->id,
5290 $mform);
5291 $o .= $this->get_renderer()->render($submitforgradingpage);
5292 $o .= $this->view_footer();
5294 \mod_assign\event\submission_confirmation_form_viewed::create_from_assign($this)->trigger();
5296 return $o;
5300 * Creates an assign_submission_status renderable.
5302 * @param stdClass $user the user to get the report for
5303 * @param bool $showlinks return plain text or links to the profile
5304 * @return assign_submission_status renderable object
5306 public function get_assign_submission_status_renderable($user, $showlinks) {
5307 global $PAGE;
5309 $instance = $this->get_instance();
5310 $flags = $this->get_user_flags($user->id, false);
5311 $submission = $this->get_user_submission($user->id, false);
5313 $teamsubmission = null;
5314 $submissiongroup = null;
5315 $notsubmitted = array();
5316 if ($instance->teamsubmission) {
5317 $teamsubmission = $this->get_group_submission($user->id, 0, false);
5318 $submissiongroup = $this->get_submission_group($user->id);
5319 $groupid = 0;
5320 if ($submissiongroup) {
5321 $groupid = $submissiongroup->id;
5323 $notsubmitted = $this->get_submission_group_members_who_have_not_submitted($groupid, false);
5326 $showedit = $showlinks &&
5327 ($this->is_any_submission_plugin_enabled()) &&
5328 $this->can_edit_submission($user->id);
5330 $submissionlocked = ($flags && $flags->locked);
5332 // Grading criteria preview.
5333 $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
5334 $gradingcontrollerpreview = '';
5335 if ($gradingmethod = $gradingmanager->get_active_method()) {
5336 $controller = $gradingmanager->get_controller($gradingmethod);
5337 if ($controller->is_form_defined()) {
5338 $gradingcontrollerpreview = $controller->render_preview($PAGE);
5342 $showsubmit = ($showlinks && $this->submissions_open($user->id));
5343 $showsubmit = ($showsubmit && $this->show_submit_button($submission, $teamsubmission, $user->id));
5345 $extensionduedate = null;
5346 if ($flags) {
5347 $extensionduedate = $flags->extensionduedate;
5349 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5351 $gradingstatus = $this->get_grading_status($user->id);
5352 $usergroups = $this->get_all_groups($user->id);
5353 $submissionstatus = new assign_submission_status($instance->allowsubmissionsfromdate,
5354 $instance->alwaysshowdescription,
5355 $submission,
5356 $instance->teamsubmission,
5357 $teamsubmission,
5358 $submissiongroup,
5359 $notsubmitted,
5360 $this->is_any_submission_plugin_enabled(),
5361 $submissionlocked,
5362 $this->is_graded($user->id),
5363 $instance->duedate,
5364 $instance->cutoffdate,
5365 $this->get_submission_plugins(),
5366 $this->get_return_action(),
5367 $this->get_return_params(),
5368 $this->get_course_module()->id,
5369 $this->get_course()->id,
5370 assign_submission_status::STUDENT_VIEW,
5371 $showedit,
5372 $showsubmit,
5373 $viewfullnames,
5374 $extensionduedate,
5375 $this->get_context(),
5376 $this->is_blind_marking(),
5377 $gradingcontrollerpreview,
5378 $instance->attemptreopenmethod,
5379 $instance->maxattempts,
5380 $gradingstatus,
5381 $instance->preventsubmissionnotingroup,
5382 $usergroups,
5383 $instance->timelimit);
5384 return $submissionstatus;
5389 * Creates an assign_feedback_status renderable.
5391 * @param stdClass $user the user to get the report for
5392 * @return assign_feedback_status renderable object
5394 public function get_assign_feedback_status_renderable($user) {
5395 global $CFG, $DB, $PAGE;
5397 require_once($CFG->libdir.'/gradelib.php');
5398 require_once($CFG->dirroot.'/grade/grading/lib.php');
5400 $instance = $this->get_instance();
5401 $grade = $this->get_user_grade($user->id, false);
5402 $gradingstatus = $this->get_grading_status($user->id);
5404 $gradinginfo = grade_get_grades($this->get_course()->id,
5405 'mod',
5406 'assign',
5407 $instance->id,
5408 $user->id);
5410 $gradingitem = null;
5411 $gradebookgrade = null;
5412 if (isset($gradinginfo->items[0])) {
5413 $gradingitem = $gradinginfo->items[0];
5414 $gradebookgrade = $gradingitem->grades[$user->id];
5417 // Check to see if all feedback plugins are empty.
5418 $emptyplugins = true;
5419 if ($grade) {
5420 foreach ($this->get_feedback_plugins() as $plugin) {
5421 if ($plugin->is_visible() && $plugin->is_enabled()) {
5422 if (!$plugin->is_empty($grade)) {
5423 $emptyplugins = false;
5429 if ($this->get_instance()->markingworkflow && $gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
5430 $emptyplugins = true; // Don't show feedback plugins until released either.
5433 $cangrade = has_capability('mod/assign:grade', $this->get_context());
5434 $hasgrade = $this->get_instance()->grade != GRADE_TYPE_NONE &&
5435 !is_null($gradebookgrade) && !is_null($gradebookgrade->grade);
5436 $gradevisible = $cangrade || $this->get_instance()->grade == GRADE_TYPE_NONE ||
5437 (!is_null($gradebookgrade) && !$gradebookgrade->hidden);
5438 // If there is a visible grade, show the summary.
5439 if (($hasgrade || !$emptyplugins) && $gradevisible) {
5441 $gradefordisplay = null;
5442 $gradeddate = null;
5443 $grader = null;
5444 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5446 $gradingcontrollergrade = '';
5447 if ($hasgrade) {
5448 if ($controller = $gradingmanager->get_active_controller()) {
5449 $menu = make_grades_menu($this->get_instance()->grade);
5450 $controller->set_grade_range($menu, $this->get_instance()->grade > 0);
5451 $gradingcontrollergrade = $controller->render_grade(
5452 $PAGE,
5453 $grade->id,
5454 $gradingitem,
5456 $cangrade
5458 $gradefordisplay = $gradebookgrade->str_long_grade;
5459 } else {
5460 $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
5462 $gradeddate = $gradebookgrade->dategraded;
5464 // Only display the grader if it is in the right state.
5465 if (in_array($gradingstatus, [ASSIGN_GRADING_STATUS_GRADED, ASSIGN_MARKING_WORKFLOW_STATE_RELEASED])){
5466 if (isset($grade->grader) && $grade->grader > 0) {
5467 $grader = $DB->get_record('user', array('id' => $grade->grader));
5468 } else if (isset($gradebookgrade->usermodified)
5469 && $gradebookgrade->usermodified > 0
5470 && has_capability('mod/assign:grade', $this->get_context(), $gradebookgrade->usermodified)) {
5471 // Grader not provided. Check that usermodified is a user who can grade.
5472 // Case 1: When an assignment is reopened an empty assign_grade is created so the feedback
5473 // plugin can know which attempt it's referring to. In this case, usermodifed is a student.
5474 // Case 2: When an assignment's grade is overrided via the gradebook, usermodified is a grader
5475 $grader = $DB->get_record('user', array('id' => $gradebookgrade->usermodified));
5480 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_context());
5482 if ($grade) {
5483 \mod_assign\event\feedback_viewed::create_from_grade($this, $grade)->trigger();
5485 $feedbackstatus = new assign_feedback_status(
5486 $gradefordisplay,
5487 $gradeddate,
5488 $grader,
5489 $this->get_feedback_plugins(),
5490 $grade,
5491 $this->get_course_module()->id,
5492 $this->get_return_action(),
5493 $this->get_return_params(),
5494 $viewfullnames,
5495 $gradingcontrollergrade
5498 // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability.
5499 $showgradername = (
5500 has_capability('mod/assign:showhiddengrader', $this->context) or
5501 !$this->is_hidden_grader()
5504 if (!$showgradername) {
5505 $feedbackstatus->grader = false;
5508 return $feedbackstatus;
5510 return;
5514 * Creates an assign_attempt_history renderable.
5516 * @param stdClass $user the user to get the report for
5517 * @return assign_attempt_history renderable object
5519 public function get_assign_attempt_history_renderable($user) {
5521 $allsubmissions = $this->get_all_submissions($user->id);
5522 $allgrades = $this->get_all_grades($user->id);
5524 $history = new assign_attempt_history($allsubmissions,
5525 $allgrades,
5526 $this->get_submission_plugins(),
5527 $this->get_feedback_plugins(),
5528 $this->get_course_module()->id,
5529 $this->get_return_action(),
5530 $this->get_return_params(),
5531 false,
5534 return $history;
5538 * Print 2 tables of information with no action links -
5539 * the submission summary and the grading summary.
5541 * @param stdClass $user the user to print the report for
5542 * @param bool $showlinks - Return plain text or links to the profile
5543 * @return string - the html summary
5545 public function view_student_summary($user, $showlinks) {
5547 $o = '';
5549 if ($this->can_view_submission($user->id)) {
5550 if (has_capability('mod/assign:viewownsubmissionsummary', $this->get_context(), $user, false)) {
5551 // The user can view the submission summary.
5552 $submissionstatus = $this->get_assign_submission_status_renderable($user, $showlinks);
5553 $o .= $this->get_renderer()->render($submissionstatus);
5556 // If there is a visible grade, show the feedback.
5557 $feedbackstatus = $this->get_assign_feedback_status_renderable($user);
5558 if ($feedbackstatus) {
5559 $o .= $this->get_renderer()->render($feedbackstatus);
5562 // If there is more than one submission, show the history.
5563 $history = $this->get_assign_attempt_history_renderable($user);
5564 if (count($history->submissions) > 1) {
5565 $o .= $this->get_renderer()->render($history);
5568 return $o;
5572 * Returns true if the submit subsission button should be shown to the user.
5574 * @param stdClass $submission The users own submission record.
5575 * @param stdClass $teamsubmission The users team submission record if there is one
5576 * @param int $userid The user
5577 * @return bool
5579 protected function show_submit_button($submission = null, $teamsubmission = null, $userid = null) {
5580 if (!has_capability('mod/assign:submit', $this->get_context(), $userid, false)) {
5581 // The user does not have the capability to submit.
5582 return false;
5584 if ($teamsubmission) {
5585 if ($teamsubmission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5586 // The assignment submission has been completed.
5587 return false;
5588 } else if ($this->submission_empty($teamsubmission)) {
5589 // There is nothing to submit yet.
5590 return false;
5591 } else if ($submission && $submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5592 // The user has already clicked the submit button on the team submission.
5593 return false;
5594 } else if (
5595 !empty($this->get_instance()->preventsubmissionnotingroup)
5596 && $this->get_submission_group($userid) == false
5598 return false;
5600 } else if ($submission) {
5601 if ($submission->status === ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
5602 // The assignment submission has been completed.
5603 return false;
5604 } else if ($this->submission_empty($submission)) {
5605 // There is nothing to submit.
5606 return false;
5608 } else {
5609 // We've not got a valid submission or team submission.
5610 return false;
5612 // Last check is that this instance allows drafts.
5613 return $this->get_instance()->submissiondrafts;
5617 * Get the grades for all previous attempts.
5618 * For each grade - the grader is a full user record,
5619 * and gradefordisplay is added (rendered from grading manager).
5621 * @param int $userid If not set, $USER->id will be used.
5622 * @return array $grades All grade records for this user.
5624 protected function get_all_grades($userid) {
5625 global $DB, $USER, $PAGE;
5627 // If the userid is not null then use userid.
5628 if (!$userid) {
5629 $userid = $USER->id;
5632 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
5634 $grades = $DB->get_records('assign_grades', $params, 'attemptnumber ASC');
5636 $gradercache = array();
5637 $cangrade = has_capability('mod/assign:grade', $this->get_context());
5639 // Show the grader's identity if 'Hide Grader' is disabled or has the 'Show Hidden Grader' capability.
5640 $showgradername = (
5641 has_capability('mod/assign:showhiddengrader', $this->context, $userid) or
5642 !$this->is_hidden_grader()
5645 // Need gradingitem and gradingmanager.
5646 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
5647 $controller = $gradingmanager->get_active_controller();
5649 $gradinginfo = grade_get_grades($this->get_course()->id,
5650 'mod',
5651 'assign',
5652 $this->get_instance()->id,
5653 $userid);
5655 $gradingitem = null;
5656 if (isset($gradinginfo->items[0])) {
5657 $gradingitem = $gradinginfo->items[0];
5660 foreach ($grades as $grade) {
5661 // First lookup the grader info.
5662 if (!$showgradername) {
5663 $grade->grader = null;
5664 } else if (isset($gradercache[$grade->grader])) {
5665 $grade->grader = $gradercache[$grade->grader];
5666 } else if ($grade->grader > 0) {
5667 // Not in cache - need to load the grader record.
5668 $grade->grader = $DB->get_record('user', array('id'=>$grade->grader));
5669 if ($grade->grader) {
5670 $gradercache[$grade->grader->id] = $grade->grader;
5674 // Now get the gradefordisplay.
5675 if ($controller) {
5676 $controller->set_grade_range(make_grades_menu($this->get_instance()->grade), $this->get_instance()->grade > 0);
5677 $grade->gradefordisplay = $controller->render_grade($PAGE,
5678 $grade->id,
5679 $gradingitem,
5680 $grade->grade,
5681 $cangrade);
5682 } else {
5683 $grade->gradefordisplay = $this->display_grade($grade->grade, false);
5688 return $grades;
5692 * Get the submissions for all previous attempts.
5694 * @param int $userid If not set, $USER->id will be used.
5695 * @return array $submissions All submission records for this user (or group).
5697 public function get_all_submissions($userid) {
5698 global $DB, $USER;
5700 // If the userid is not null then use userid.
5701 if (!$userid) {
5702 $userid = $USER->id;
5705 $params = array();
5707 if ($this->get_instance()->teamsubmission) {
5708 $groupid = 0;
5709 $group = $this->get_submission_group($userid);
5710 if ($group) {
5711 $groupid = $group->id;
5714 // Params to get the group submissions.
5715 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0);
5716 } else {
5717 // Params to get the user submissions.
5718 $params = array('assignment'=>$this->get_instance()->id, 'userid'=>$userid);
5721 // Return the submissions ordered by attempt.
5722 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber ASC');
5724 return $submissions;
5728 * Creates an assign_grading_summary renderable.
5730 * @param mixed $activitygroup int|null the group for calculating the grading summary (if null the function will determine it)
5731 * @return assign_grading_summary renderable object
5733 public function get_assign_grading_summary_renderable($activitygroup = null) {
5735 $instance = $this->get_default_instance(); // Grading summary requires the raw dates, regardless of relativedates mode.
5736 $cm = $this->get_course_module();
5737 $course = $this->get_course();
5739 $draft = ASSIGN_SUBMISSION_STATUS_DRAFT;
5740 $submitted = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
5741 $isvisible = $cm->visible;
5743 if ($activitygroup === null) {
5744 $activitygroup = groups_get_activity_group($cm);
5747 if ($instance->teamsubmission) {
5748 $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_NO;
5749 $defaultteammembers = $this->get_submission_group_members(0, true);
5750 if (count($defaultteammembers) > 0) {
5751 if ($instance->preventsubmissionnotingroup) {
5752 $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_REQUIRED;
5753 } else {
5754 $warnofungroupedusers = assign_grading_summary::WARN_GROUPS_OPTIONAL;
5758 $summary = new assign_grading_summary(
5759 $this->count_teams($activitygroup),
5760 $instance->submissiondrafts,
5761 $this->count_submissions_with_status($draft, $activitygroup),
5762 $this->is_any_submission_plugin_enabled(),
5763 $this->count_submissions_with_status($submitted, $activitygroup),
5764 $this->get_cutoffdate($activitygroup),
5765 $this->get_duedate($activitygroup),
5766 $this->get_timelimit($activitygroup),
5767 $this->get_course_module()->id,
5768 $this->count_submissions_need_grading($activitygroup),
5769 $instance->teamsubmission,
5770 $warnofungroupedusers,
5771 $course->relativedatesmode,
5772 $course->startdate,
5773 $this->can_grade(),
5774 $isvisible,
5775 $this->get_course_module()
5777 } else {
5778 // The active group has already been updated in groups_print_activity_menu().
5779 $countparticipants = $this->count_participants($activitygroup);
5780 $summary = new assign_grading_summary(
5781 $countparticipants,
5782 $instance->submissiondrafts,
5783 $this->count_submissions_with_status($draft, $activitygroup),
5784 $this->is_any_submission_plugin_enabled(),
5785 $this->count_submissions_with_status($submitted, $activitygroup),
5786 $this->get_cutoffdate($activitygroup),
5787 $this->get_duedate($activitygroup),
5788 $this->get_timelimit($activitygroup),
5789 $this->get_course_module()->id,
5790 $this->count_submissions_need_grading($activitygroup),
5791 $instance->teamsubmission,
5792 assign_grading_summary::WARN_GROUPS_NO,
5793 $course->relativedatesmode,
5794 $course->startdate,
5795 $this->can_grade(),
5796 $isvisible,
5797 $this->get_course_module()
5801 return $summary;
5805 * Helper function to allow up to fetch the group overrides via one query as opposed to many calls.
5807 * @param int $activitygroup The group we want to check the overrides of
5808 * @return mixed Can return either a fetched DB object, local object or false
5810 private function get_override_data(int $activitygroup) {
5811 global $DB;
5813 $instanceid = $this->get_instance()->id;
5814 $cachekey = "$instanceid-$activitygroup";
5815 if (isset($this->overridedata[$cachekey])) {
5816 return $this->overridedata[$cachekey];
5819 $params = ['groupid' => $activitygroup, 'assignid' => $instanceid];
5820 $this->overridedata[$cachekey] = $DB->get_record('assign_overrides', $params);
5821 return $this->overridedata[$cachekey];
5825 * Return group override duedate.
5827 * @param int $activitygroup Activity active group
5828 * @return int $duedate
5830 private function get_duedate($activitygroup = null) {
5831 if ($activitygroup === null) {
5832 $activitygroup = groups_get_activity_group($this->get_course_module());
5834 if ($this->can_view_grades() && !empty($activitygroup)) {
5835 $groupoverride = $this->get_override_data($activitygroup);
5836 if (!empty($groupoverride->duedate)) {
5837 return $groupoverride->duedate;
5840 return $this->get_instance()->duedate;
5844 * Return group override timelimit.
5846 * @param null|int $activitygroup Activity active group
5847 * @return int $timelimit
5849 private function get_timelimit(?int $activitygroup = null): int {
5850 if ($activitygroup === null) {
5851 $activitygroup = groups_get_activity_group($this->get_course_module());
5853 if ($this->can_view_grades() && !empty($activitygroup)) {
5854 $groupoverride = $this->get_override_data($activitygroup);
5855 if (!empty($groupoverride->timelimit)) {
5856 return $groupoverride->timelimit;
5859 return $this->get_instance()->timelimit;
5863 * Return group override cutoffdate.
5865 * @param null|int $activitygroup Activity active group
5866 * @return int $cutoffdate
5868 private function get_cutoffdate(?int $activitygroup = null): int {
5869 if ($activitygroup === null) {
5870 $activitygroup = groups_get_activity_group($this->get_course_module());
5872 if ($this->can_view_grades() && !empty($activitygroup)) {
5873 $groupoverride = $this->get_override_data($activitygroup);
5874 if (!empty($groupoverride->cutoffdate)) {
5875 return $groupoverride->cutoffdate;
5878 return $this->get_instance()->cutoffdate;
5882 * View submissions page (contains details of current submission).
5884 * @return string
5886 protected function view_submission_page() {
5887 global $CFG, $DB, $USER, $PAGE;
5889 $instance = $this->get_instance();
5891 $this->add_grade_notices();
5893 $o = '';
5895 $postfix = '';
5896 if ($this->has_visible_attachments() && (!$this->get_instance($USER->id)->submissionattachments)) {
5897 $postfix = $this->render_area_files('mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0);
5900 $o .= $this->get_renderer()->render(new assign_header($instance,
5901 $this->get_context(),
5902 $this->show_intro(),
5903 $this->get_course_module()->id,
5904 '', '', $postfix));
5906 // Display plugin specific headers.
5907 $plugins = array_merge($this->get_submission_plugins(), $this->get_feedback_plugins());
5908 foreach ($plugins as $plugin) {
5909 if ($plugin->is_enabled() && $plugin->is_visible()) {
5910 $o .= $this->get_renderer()->render(new assign_plugin_header($plugin));
5914 if ($this->can_view_grades()) {
5915 $actionbuttons = new \mod_assign\output\actionmenu($this->get_course_module()->id);
5916 $o .= $this->get_renderer()->submission_actionmenu($actionbuttons);
5918 $summary = $this->get_assign_grading_summary_renderable();
5919 $o .= $this->get_renderer()->render($summary);
5922 if ($this->can_view_submission($USER->id)) {
5923 $o .= $this->view_submission_action_bar($instance, $USER);
5924 $o .= $this->view_student_summary($USER, true);
5927 $o .= $this->view_footer();
5929 \mod_assign\event\submission_status_viewed::create_from_assign($this)->trigger();
5931 return $o;
5935 * The action bar displayed in the submissions page.
5937 * @param stdClass $instance The settings for the current instance of this assignment
5938 * @param stdClass $user The user to print the action bar for
5939 * @return string
5941 public function view_submission_action_bar(stdClass $instance, stdClass $user): string {
5942 $submission = $this->get_user_submission($user->id, false);
5943 // Figure out if we are team or solitary submission.
5944 $teamsubmission = null;
5945 if ($instance->teamsubmission) {
5946 $teamsubmission = $this->get_group_submission($user->id, 0, false);
5949 $showsubmit = ($this->submissions_open($user->id)
5950 && $this->show_submit_button($submission, $teamsubmission, $user->id));
5951 $showedit = ($this->is_any_submission_plugin_enabled()) && $this->can_edit_submission($user->id);
5953 // The method get_group_submission() says that it returns a stdClass, but it can return false >_>.
5954 if ($teamsubmission === false) {
5955 $teamsubmission = new stdClass();
5957 // Same goes for get_user_submission().
5958 if ($submission === false) {
5959 $submission = new stdClass();
5961 $actionbuttons = new \mod_assign\output\user_submission_actionmenu(
5962 $this->get_course_module()->id,
5963 $showsubmit,
5964 $showedit,
5965 $submission,
5966 $teamsubmission,
5967 $instance->timelimit
5970 return $this->get_renderer()->render($actionbuttons);
5974 * Convert the final raw grade(s) in the grading table for the gradebook.
5976 * @param stdClass $grade
5977 * @return array
5979 protected function convert_grade_for_gradebook(stdClass $grade) {
5980 $gradebookgrade = array();
5981 if ($grade->grade >= 0) {
5982 $gradebookgrade['rawgrade'] = $grade->grade;
5984 // Allow "no grade" to be chosen.
5985 if ($grade->grade == -1) {
5986 $gradebookgrade['rawgrade'] = NULL;
5988 $gradebookgrade['userid'] = $grade->userid;
5989 $gradebookgrade['usermodified'] = $grade->grader;
5990 $gradebookgrade['datesubmitted'] = null;
5991 $gradebookgrade['dategraded'] = $grade->timemodified;
5992 if (isset($grade->feedbackformat)) {
5993 $gradebookgrade['feedbackformat'] = $grade->feedbackformat;
5995 if (isset($grade->feedbacktext)) {
5996 $gradebookgrade['feedback'] = $grade->feedbacktext;
5998 if (isset($grade->feedbackfiles)) {
5999 $gradebookgrade['feedbackfiles'] = $grade->feedbackfiles;
6002 return $gradebookgrade;
6006 * Convert submission details for the gradebook.
6008 * @param stdClass $submission
6009 * @return array
6011 protected function convert_submission_for_gradebook(stdClass $submission) {
6012 $gradebookgrade = array();
6014 $gradebookgrade['userid'] = $submission->userid;
6015 $gradebookgrade['usermodified'] = $submission->userid;
6016 $gradebookgrade['datesubmitted'] = $submission->timemodified;
6018 return $gradebookgrade;
6022 * Update grades in the gradebook.
6024 * @param mixed $submission stdClass|null
6025 * @param mixed $grade stdClass|null
6026 * @return bool
6028 protected function gradebook_item_update($submission=null, $grade=null) {
6029 global $CFG;
6031 require_once($CFG->dirroot.'/mod/assign/lib.php');
6032 // Do not push grade to gradebook if blind marking is active as
6033 // the gradebook would reveal the students.
6034 if ($this->is_blind_marking()) {
6035 return false;
6038 // If marking workflow is enabled and grade is not released then remove any grade that may exist in the gradebook.
6039 if ($this->get_instance()->markingworkflow && !empty($grade) &&
6040 $this->get_grading_status($grade->userid) != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
6041 // Remove the grade (if it exists) from the gradebook as it is not 'final'.
6042 $grade->grade = -1;
6043 $grade->feedbacktext = '';
6044 $grade->feebackfiles = [];
6047 if ($submission != null) {
6048 if ($submission->userid == 0) {
6049 // This is a group submission update.
6050 $team = groups_get_members($submission->groupid, 'u.id');
6052 foreach ($team as $member) {
6053 $membersubmission = clone $submission;
6054 $membersubmission->groupid = 0;
6055 $membersubmission->userid = $member->id;
6056 $this->gradebook_item_update($membersubmission, null);
6058 return;
6061 $gradebookgrade = $this->convert_submission_for_gradebook($submission);
6063 } else {
6064 $gradebookgrade = $this->convert_grade_for_gradebook($grade);
6066 // Grading is disabled, return.
6067 if ($this->grading_disabled($gradebookgrade['userid'])) {
6068 return false;
6070 $assign = clone $this->get_instance();
6071 $assign->cmidnumber = $this->get_course_module()->idnumber;
6072 // Set assign gradebook feedback plugin status (enabled and visible).
6073 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
6074 return assign_grade_item_update($assign, $gradebookgrade) == GRADE_UPDATE_OK;
6078 * Update team submission.
6080 * @param stdClass $submission
6081 * @param int $userid
6082 * @param bool $updatetime
6083 * @return bool
6085 protected function update_team_submission(stdClass $submission, $userid, $updatetime) {
6086 global $DB;
6088 if ($updatetime) {
6089 $submission->timemodified = time();
6092 // First update the submission for the current user.
6093 $mysubmission = $this->get_user_submission($userid, true, $submission->attemptnumber);
6094 $mysubmission->status = $submission->status;
6096 $this->update_submission($mysubmission, 0, $updatetime, false);
6098 // Now check the team settings to see if this assignment qualifies as submitted or draft.
6099 $team = $this->get_submission_group_members($submission->groupid, true);
6101 $allsubmitted = true;
6102 $anysubmitted = false;
6103 $result = true;
6104 if (!in_array($submission->status, [ASSIGN_SUBMISSION_STATUS_NEW, ASSIGN_SUBMISSION_STATUS_REOPENED])) {
6105 foreach ($team as $member) {
6106 $membersubmission = $this->get_user_submission($member->id, false, $submission->attemptnumber);
6108 // If no submission found for team member and member is active then everyone has not submitted.
6109 if (!$membersubmission || $membersubmission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED
6110 && ($this->is_active_user($member->id))) {
6111 $allsubmitted = false;
6112 if ($anysubmitted) {
6113 break;
6115 } else {
6116 $anysubmitted = true;
6119 if ($this->get_instance()->requireallteammemberssubmit) {
6120 if ($allsubmitted) {
6121 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6122 } else {
6123 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
6125 $result = $DB->update_record('assign_submission', $submission);
6126 } else {
6127 if ($anysubmitted) {
6128 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6129 } else {
6130 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
6132 $result = $DB->update_record('assign_submission', $submission);
6134 } else {
6135 // Set the group submission to reopened.
6136 foreach ($team as $member) {
6137 $membersubmission = $this->get_user_submission($member->id, true, $submission->attemptnumber);
6138 $membersubmission->status = $submission->status;
6139 $result = $DB->update_record('assign_submission', $membersubmission) && $result;
6141 $result = $DB->update_record('assign_submission', $submission) && $result;
6144 $this->gradebook_item_update($submission);
6145 return $result;
6149 * Update grades in the gradebook based on submission time.
6151 * @param stdClass $submission
6152 * @param int $userid
6153 * @param bool $updatetime
6154 * @param bool $teamsubmission
6155 * @return bool
6157 protected function update_submission(stdClass $submission, $userid, $updatetime, $teamsubmission) {
6158 global $DB;
6160 if ($teamsubmission) {
6161 return $this->update_team_submission($submission, $userid, $updatetime);
6164 if ($updatetime) {
6165 $submission->timemodified = time();
6167 $result= $DB->update_record('assign_submission', $submission);
6168 if ($result) {
6169 $this->gradebook_item_update($submission);
6171 return $result;
6175 * Is this assignment open for submissions?
6177 * Check the due date,
6178 * prevent late submissions,
6179 * has this person already submitted,
6180 * is the assignment locked?
6182 * @param int $userid - Optional userid so we can see if a different user can submit
6183 * @param bool $skipenrolled - Skip enrollment checks (because they have been done already)
6184 * @param stdClass $submission - Pre-fetched submission record (or false to fetch it)
6185 * @param stdClass $flags - Pre-fetched user flags record (or false to fetch it)
6186 * @param stdClass $gradinginfo - Pre-fetched user gradinginfo record (or false to fetch it)
6187 * @return bool
6189 public function submissions_open($userid = 0,
6190 $skipenrolled = false,
6191 $submission = false,
6192 $flags = false,
6193 $gradinginfo = false) {
6194 global $USER;
6196 if (!$userid) {
6197 $userid = $USER->id;
6200 $time = time();
6201 $dateopen = true;
6202 $finaldate = false;
6203 if ($this->get_instance()->cutoffdate) {
6204 $finaldate = $this->get_instance()->cutoffdate;
6207 if ($flags === false) {
6208 $flags = $this->get_user_flags($userid, false);
6210 if ($flags && $flags->locked) {
6211 return false;
6214 // User extensions.
6215 if ($finaldate) {
6216 if ($flags && $flags->extensionduedate) {
6217 // Extension can be before cut off date.
6218 if ($flags->extensionduedate > $finaldate) {
6219 $finaldate = $flags->extensionduedate;
6224 if ($finaldate) {
6225 $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time && $time <= $finaldate);
6226 } else {
6227 $dateopen = ($this->get_instance()->allowsubmissionsfromdate <= $time);
6230 if (!$dateopen) {
6231 return false;
6234 // Now check if this user has already submitted etc.
6235 if (!$skipenrolled && !is_enrolled($this->get_course_context(), $userid)) {
6236 return false;
6238 // Note you can pass null for submission and it will not be fetched.
6239 if ($submission === false) {
6240 if ($this->get_instance()->teamsubmission) {
6241 $submission = $this->get_group_submission($userid, 0, false);
6242 } else {
6243 $submission = $this->get_user_submission($userid, false);
6246 if ($submission) {
6248 if ($this->get_instance()->submissiondrafts && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6249 // Drafts are tracked and the student has submitted the assignment.
6250 return false;
6254 // See if this user grade is locked in the gradebook.
6255 if ($gradinginfo === false) {
6256 $gradinginfo = grade_get_grades($this->get_course()->id,
6257 'mod',
6258 'assign',
6259 $this->get_instance()->id,
6260 array($userid));
6262 if ($gradinginfo &&
6263 isset($gradinginfo->items[0]->grades[$userid]) &&
6264 $gradinginfo->items[0]->grades[$userid]->locked) {
6265 return false;
6268 return true;
6272 * Render the files in file area.
6274 * @param string $component
6275 * @param string $area
6276 * @param int $submissionid
6277 * @return string
6279 public function render_area_files($component, $area, $submissionid) {
6280 global $USER;
6282 return $this->get_renderer()->assign_files($this->context, $submissionid, $area, $component,
6283 $this->course, $this->coursemodule);
6288 * Capability check to make sure this grader can edit this submission.
6290 * @param int $userid - The user whose submission is to be edited
6291 * @param int $graderid (optional) - The user who will do the editing (default to $USER->id).
6292 * @return bool
6294 public function can_edit_submission($userid, $graderid = 0) {
6295 global $USER;
6297 if (empty($graderid)) {
6298 $graderid = $USER->id;
6301 $instance = $this->get_instance();
6302 if ($userid == $graderid &&
6303 $instance->teamsubmission &&
6304 $instance->preventsubmissionnotingroup &&
6305 $this->get_submission_group($userid) == false) {
6306 return false;
6309 if ($userid == $graderid) {
6310 if ($this->submissions_open($userid) &&
6311 has_capability('mod/assign:submit', $this->context, $graderid)) {
6312 // User can edit their own submission.
6313 return true;
6314 } else {
6315 // We need to return here because editothersubmission should never apply to a users own submission.
6316 return false;
6320 if (!has_capability('mod/assign:editothersubmission', $this->context, $graderid)) {
6321 return false;
6324 $cm = $this->get_course_module();
6325 if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
6326 $sharedgroupmembers = $this->get_shared_group_members($cm, $graderid);
6327 return in_array($userid, $sharedgroupmembers);
6329 return true;
6333 * Returns IDs of the users who share group membership with the specified user.
6335 * @param stdClass|cm_info $cm Course-module
6336 * @param int $userid User ID
6337 * @return array An array of ID of users.
6339 public function get_shared_group_members($cm, $userid) {
6340 if (!isset($this->sharedgroupmembers[$userid])) {
6341 $this->sharedgroupmembers[$userid] = array();
6342 if ($members = groups_get_activity_shared_group_members($cm, $userid)) {
6343 $this->sharedgroupmembers[$userid] = array_keys($members);
6347 return $this->sharedgroupmembers[$userid];
6351 * Returns a list of teachers that should be grading given submission.
6353 * @param int $userid The submission to grade
6354 * @return array
6356 protected function get_graders($userid) {
6357 // Potential graders should be active users only.
6358 $potentialgraders = get_enrolled_users($this->context, "mod/assign:grade", null, 'u.*', null, null, null, true);
6360 $graders = array();
6361 if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
6362 if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
6363 foreach ($groups as $group) {
6364 foreach ($potentialgraders as $grader) {
6365 if ($grader->id == $userid) {
6366 // Do not send self.
6367 continue;
6369 if (groups_is_member($group->id, $grader->id)) {
6370 $graders[$grader->id] = $grader;
6374 } else {
6375 // User not in group, try to find graders without group.
6376 foreach ($potentialgraders as $grader) {
6377 if ($grader->id == $userid) {
6378 // Do not send self.
6379 continue;
6381 if (!groups_has_membership($this->get_course_module(), $grader->id)) {
6382 $graders[$grader->id] = $grader;
6386 } else {
6387 foreach ($potentialgraders as $grader) {
6388 if ($grader->id == $userid) {
6389 // Do not send self.
6390 continue;
6392 // Must be enrolled.
6393 if (is_enrolled($this->get_course_context(), $grader->id)) {
6394 $graders[$grader->id] = $grader;
6398 return $graders;
6402 * Returns a list of users that should receive notification about given submission.
6404 * @param int $userid The submission to grade
6405 * @return array
6407 protected function get_notifiable_users($userid) {
6408 // Potential users should be active users only.
6409 $potentialusers = get_enrolled_users($this->context, "mod/assign:receivegradernotifications",
6410 null, 'u.*', null, null, null, true);
6412 $notifiableusers = array();
6413 if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
6414 if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
6415 foreach ($groups as $group) {
6416 foreach ($potentialusers as $potentialuser) {
6417 if ($potentialuser->id == $userid) {
6418 // Do not send self.
6419 continue;
6421 if (groups_is_member($group->id, $potentialuser->id)) {
6422 $notifiableusers[$potentialuser->id] = $potentialuser;
6426 } else {
6427 // User not in group, try to find graders without group.
6428 foreach ($potentialusers as $potentialuser) {
6429 if ($potentialuser->id == $userid) {
6430 // Do not send self.
6431 continue;
6433 if (!groups_has_membership($this->get_course_module(), $potentialuser->id)) {
6434 $notifiableusers[$potentialuser->id] = $potentialuser;
6438 } else {
6439 foreach ($potentialusers as $potentialuser) {
6440 if ($potentialuser->id == $userid) {
6441 // Do not send self.
6442 continue;
6444 // Must be enrolled.
6445 if (is_enrolled($this->get_course_context(), $potentialuser->id)) {
6446 $notifiableusers[$potentialuser->id] = $potentialuser;
6450 return $notifiableusers;
6454 * Format a notification for plain text.
6456 * @param string $messagetype
6457 * @param stdClass $info
6458 * @param stdClass $course
6459 * @param stdClass $context
6460 * @param string $modulename
6461 * @param string $assignmentname
6463 protected static function format_notification_message_text($messagetype,
6464 $info,
6465 $course,
6466 $context,
6467 $modulename,
6468 $assignmentname) {
6469 $formatparams = array('context' => $context->get_course_context());
6470 $posttext = format_string($course->shortname, true, $formatparams) .
6471 ' -> ' .
6472 $modulename .
6473 ' -> ' .
6474 format_string($assignmentname, true, $formatparams) . "\n";
6475 $posttext .= '---------------------------------------------------------------------' . "\n";
6476 $posttext .= get_string($messagetype . 'text', 'assign', $info)."\n";
6477 $posttext .= "\n---------------------------------------------------------------------\n";
6478 return $posttext;
6482 * Format a notification for HTML.
6484 * @param string $messagetype
6485 * @param stdClass $info
6486 * @param stdClass $course
6487 * @param stdClass $context
6488 * @param string $modulename
6489 * @param stdClass $coursemodule
6490 * @param string $assignmentname
6492 protected static function format_notification_message_html($messagetype,
6493 $info,
6494 $course,
6495 $context,
6496 $modulename,
6497 $coursemodule,
6498 $assignmentname) {
6499 global $CFG;
6500 $formatparams = array('context' => $context->get_course_context());
6501 $posthtml = '<p><font face="sans-serif">' .
6502 '<a href="' . $CFG->wwwroot . '/course/view.php?id=' . $course->id . '">' .
6503 format_string($course->shortname, true, $formatparams) .
6504 '</a> ->' .
6505 '<a href="' . $CFG->wwwroot . '/mod/assign/index.php?id=' . $course->id . '">' .
6506 $modulename .
6507 '</a> ->' .
6508 '<a href="' . $CFG->wwwroot . '/mod/assign/view.php?id=' . $coursemodule->id . '">' .
6509 format_string($assignmentname, true, $formatparams) .
6510 '</a></font></p>';
6511 $posthtml .= '<hr /><font face="sans-serif">';
6512 $posthtml .= '<p>' . get_string($messagetype . 'html', 'assign', $info) . '</p>';
6513 $posthtml .= '</font><hr />';
6514 return $posthtml;
6518 * Message someone about something (static so it can be called from cron).
6520 * @param stdClass $userfrom
6521 * @param stdClass $userto
6522 * @param string $messagetype
6523 * @param string $eventtype
6524 * @param int $updatetime
6525 * @param stdClass $coursemodule
6526 * @param stdClass $context
6527 * @param stdClass $course
6528 * @param string $modulename
6529 * @param string $assignmentname
6530 * @param bool $blindmarking
6531 * @param int $uniqueidforuser
6532 * @return void
6534 public static function send_assignment_notification($userfrom,
6535 $userto,
6536 $messagetype,
6537 $eventtype,
6538 $updatetime,
6539 $coursemodule,
6540 $context,
6541 $course,
6542 $modulename,
6543 $assignmentname,
6544 $blindmarking,
6545 $uniqueidforuser) {
6546 global $CFG, $PAGE;
6548 $info = new stdClass();
6549 if ($blindmarking) {
6550 $userfrom = clone($userfrom);
6551 $info->username = get_string('participant', 'assign') . ' ' . $uniqueidforuser;
6552 $userfrom->firstname = get_string('participant', 'assign');
6553 $userfrom->lastname = $uniqueidforuser;
6554 $userfrom->email = $CFG->noreplyaddress;
6555 } else {
6556 $info->username = fullname($userfrom, true);
6558 $info->assignment = format_string($assignmentname, true, array('context'=>$context));
6559 $info->url = $CFG->wwwroot.'/mod/assign/view.php?id='.$coursemodule->id;
6560 $info->timeupdated = userdate($updatetime, get_string('strftimerecentfull'));
6562 $postsubject = get_string($messagetype . 'small', 'assign', $info);
6563 $posttext = self::format_notification_message_text($messagetype,
6564 $info,
6565 $course,
6566 $context,
6567 $modulename,
6568 $assignmentname);
6569 $posthtml = '';
6570 if ($userto->mailformat == 1) {
6571 $posthtml = self::format_notification_message_html($messagetype,
6572 $info,
6573 $course,
6574 $context,
6575 $modulename,
6576 $coursemodule,
6577 $assignmentname);
6580 $eventdata = new \core\message\message();
6581 $eventdata->courseid = $course->id;
6582 $eventdata->modulename = 'assign';
6583 $eventdata->userfrom = $userfrom;
6584 $eventdata->userto = $userto;
6585 $eventdata->subject = $postsubject;
6586 $eventdata->fullmessage = $posttext;
6587 $eventdata->fullmessageformat = FORMAT_PLAIN;
6588 $eventdata->fullmessagehtml = $posthtml;
6589 $eventdata->smallmessage = $postsubject;
6591 $eventdata->name = $eventtype;
6592 $eventdata->component = 'mod_assign';
6593 $eventdata->notification = 1;
6594 $eventdata->contexturl = $info->url;
6595 $eventdata->contexturlname = $info->assignment;
6596 $customdata = [
6597 'cmid' => $coursemodule->id,
6598 'instance' => $coursemodule->instance,
6599 'messagetype' => $messagetype,
6600 'blindmarking' => $blindmarking,
6601 'uniqueidforuser' => $uniqueidforuser,
6603 // Check if the userfrom is real and visible.
6604 if (!empty($userfrom->id) && core_user::is_real_user($userfrom->id)) {
6605 $userpicture = new user_picture($userfrom);
6606 $userpicture->size = 1; // Use f1 size.
6607 $userpicture->includetoken = $userto->id; // Generate an out-of-session token for the user receiving the message.
6608 $customdata['notificationiconurl'] = $userpicture->get_url($PAGE)->out(false);
6610 $eventdata->customdata = $customdata;
6612 message_send($eventdata);
6616 * Message someone about something.
6618 * @param stdClass $userfrom
6619 * @param stdClass $userto
6620 * @param string $messagetype
6621 * @param string $eventtype
6622 * @param int $updatetime
6623 * @return void
6625 public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime) {
6626 global $USER;
6627 $userid = core_user::is_real_user($userfrom->id) ? $userfrom->id : $USER->id;
6628 $uniqueid = $this->get_uniqueid_for_user($userid);
6629 self::send_assignment_notification($userfrom,
6630 $userto,
6631 $messagetype,
6632 $eventtype,
6633 $updatetime,
6634 $this->get_course_module(),
6635 $this->get_context(),
6636 $this->get_course(),
6637 $this->get_module_name(),
6638 $this->get_instance()->name,
6639 $this->is_blind_marking(),
6640 $uniqueid);
6644 * Notify student upon successful submission copy.
6646 * @param stdClass $submission
6647 * @return void
6649 protected function notify_student_submission_copied(stdClass $submission) {
6650 global $DB, $USER;
6652 $adminconfig = $this->get_admin_config();
6653 // Use the same setting for this - no need for another one.
6654 if (empty($adminconfig->submissionreceipts)) {
6655 // No need to do anything.
6656 return;
6658 if ($submission->userid) {
6659 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6660 } else {
6661 $user = $USER;
6663 $this->send_notification($user,
6664 $user,
6665 'submissioncopied',
6666 'assign_notification',
6667 $submission->timemodified);
6670 * Notify student upon successful submission.
6672 * @param stdClass $submission
6673 * @return void
6675 protected function notify_student_submission_receipt(stdClass $submission) {
6676 global $DB, $USER;
6678 $adminconfig = $this->get_admin_config();
6679 if (empty($adminconfig->submissionreceipts)) {
6680 // No need to do anything.
6681 return;
6683 if ($submission->userid) {
6684 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6685 } else {
6686 $user = $USER;
6688 if ($submission->userid == $USER->id) {
6689 $this->send_notification(core_user::get_noreply_user(),
6690 $user,
6691 'submissionreceipt',
6692 'assign_notification',
6693 $submission->timemodified);
6694 } else {
6695 $this->send_notification($USER,
6696 $user,
6697 'submissionreceiptother',
6698 'assign_notification',
6699 $submission->timemodified);
6704 * Send notifications to graders upon student submissions.
6706 * @param stdClass $submission
6707 * @return void
6709 protected function notify_graders(stdClass $submission) {
6710 global $DB, $USER;
6712 $instance = $this->get_instance();
6714 $late = $instance->duedate && ($instance->duedate < time());
6716 if (!$instance->sendnotifications && !($late && $instance->sendlatenotifications)) {
6717 // No need to do anything.
6718 return;
6721 if ($submission->userid) {
6722 $user = $DB->get_record('user', array('id'=>$submission->userid), '*', MUST_EXIST);
6723 } else {
6724 $user = $USER;
6727 if ($notifyusers = $this->get_notifiable_users($user->id)) {
6728 foreach ($notifyusers as $notifyuser) {
6729 $this->send_notification($user,
6730 $notifyuser,
6731 'gradersubmissionupdated',
6732 'assign_notification',
6733 $submission->timemodified);
6739 * Submit a submission for grading.
6741 * @param stdClass $data - The form data
6742 * @param array $notices - List of error messages to display on an error condition.
6743 * @return bool Return false if the submission was not submitted.
6745 public function submit_for_grading($data, $notices) {
6746 global $USER;
6748 $userid = $USER->id;
6749 if (!empty($data->userid)) {
6750 $userid = $data->userid;
6752 // Need submit permission to submit an assignment.
6753 if ($userid == $USER->id) {
6754 require_capability('mod/assign:submit', $this->context);
6755 } else {
6756 if (!$this->can_edit_submission($userid, $USER->id)) {
6757 throw new \moodle_exception('nopermission');
6761 $instance = $this->get_instance();
6763 if ($instance->teamsubmission) {
6764 $submission = $this->get_group_submission($userid, 0, true);
6765 } else {
6766 $submission = $this->get_user_submission($userid, true);
6769 if (!$this->submissions_open($userid)) {
6770 $notices[] = get_string('submissionsclosed', 'assign');
6771 return false;
6774 if ($instance->requiresubmissionstatement && empty($data->submissionstatement) && $USER->id == $userid) {
6775 return false;
6778 if ($submission->status != ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
6779 // Give each submission plugin a chance to process the submission.
6780 $plugins = $this->get_submission_plugins();
6781 foreach ($plugins as $plugin) {
6782 if ($plugin->is_enabled() && $plugin->is_visible()) {
6783 $plugin->submit_for_grading($submission);
6787 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
6788 $this->update_submission($submission, $userid, true, $instance->teamsubmission);
6789 $completion = new completion_info($this->get_course());
6790 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
6791 $this->update_activity_completion_records($instance->teamsubmission,
6792 $instance->requireallteammemberssubmit,
6793 $submission,
6794 $userid,
6795 COMPLETION_COMPLETE,
6796 $completion);
6799 if (!empty($data->submissionstatement) && $USER->id == $userid) {
6800 \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
6802 $this->notify_graders($submission);
6803 $this->notify_student_submission_receipt($submission);
6805 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, false)->trigger();
6807 return true;
6809 $notices[] = get_string('submissionsclosed', 'assign');
6810 return false;
6814 * A students submission is submitted for grading by a teacher.
6816 * @return bool
6818 protected function process_submit_other_for_grading($mform, $notices) {
6819 global $USER, $CFG;
6821 require_sesskey();
6823 $userid = optional_param('userid', $USER->id, PARAM_INT);
6825 if (!$this->submissions_open($userid)) {
6826 $notices[] = get_string('submissionsclosed', 'assign');
6827 return false;
6829 $data = new stdClass();
6830 $data->userid = $userid;
6831 return $this->submit_for_grading($data, $notices);
6835 * Assignment submission is processed before grading.
6837 * @param moodleform|null $mform If validation failed when submitting this form - this is the moodleform.
6838 * It can be null.
6839 * @return bool Return false if the validation fails. This affects which page is displayed next.
6841 protected function process_submit_for_grading($mform, $notices) {
6842 global $CFG;
6844 require_once($CFG->dirroot . '/mod/assign/submissionconfirmform.php');
6845 require_sesskey();
6847 if (!$this->submissions_open()) {
6848 $notices[] = get_string('submissionsclosed', 'assign');
6849 return false;
6852 $data = new stdClass();
6853 $adminconfig = $this->get_admin_config();
6854 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
6856 $submissionstatement = '';
6857 if ($requiresubmissionstatement) {
6858 $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
6861 // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
6862 // that the submission statement checkbox will be displayed.
6863 if (empty($submissionstatement)) {
6864 $requiresubmissionstatement = false;
6867 if ($mform == null) {
6868 $mform = new mod_assign_confirm_submission_form(null, array($requiresubmissionstatement,
6869 $submissionstatement,
6870 $this->get_course_module()->id,
6871 $data));
6874 $data = $mform->get_data();
6875 if (!$mform->is_cancelled()) {
6876 if ($mform->get_data() == false) {
6877 return false;
6879 return $this->submit_for_grading($data, $notices);
6881 return true;
6885 * Save the extension date for a single user.
6887 * @param int $userid The user id
6888 * @param mixed $extensionduedate Either an integer date or null
6889 * @return boolean
6891 public function save_user_extension($userid, $extensionduedate) {
6892 global $DB;
6894 // Need submit permission to submit an assignment.
6895 require_capability('mod/assign:grantextension', $this->context);
6897 if (!is_enrolled($this->get_course_context(), $userid)) {
6898 return false;
6900 if (!has_capability('mod/assign:submit', $this->context, $userid)) {
6901 return false;
6904 if ($this->get_instance()->duedate && $extensionduedate) {
6905 if ($this->get_instance()->duedate > $extensionduedate) {
6906 return false;
6909 if ($this->get_instance()->allowsubmissionsfromdate && $extensionduedate) {
6910 if ($this->get_instance()->allowsubmissionsfromdate > $extensionduedate) {
6911 return false;
6915 $flags = $this->get_user_flags($userid, true);
6916 $flags->extensionduedate = $extensionduedate;
6918 $result = $this->update_user_flags($flags);
6920 if ($result) {
6921 \mod_assign\event\extension_granted::create_from_assign($this, $userid)->trigger();
6923 return $result;
6927 * Save extension date.
6929 * @param moodleform $mform The submitted form
6930 * @return boolean
6932 protected function process_save_extension(& $mform) {
6933 global $DB, $CFG;
6935 // Include extension form.
6936 require_once($CFG->dirroot . '/mod/assign/extensionform.php');
6937 require_sesskey();
6939 $users = optional_param('userid', 0, PARAM_INT);
6940 if (!$users) {
6941 $users = required_param('selectedusers', PARAM_SEQUENCE);
6943 $userlist = explode(',', $users);
6945 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate');
6946 $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0);
6947 foreach ($userlist as $userid) {
6948 // To validate extension date with users overrides.
6949 $override = $this->override_exists($userid);
6950 foreach ($keys as $key) {
6951 if ($override->{$key}) {
6952 if ($maxoverride[$key] < $override->{$key}) {
6953 $maxoverride[$key] = $override->{$key};
6955 } else if ($maxoverride[$key] < $this->get_instance()->{$key}) {
6956 $maxoverride[$key] = $this->get_instance()->{$key};
6960 foreach ($keys as $key) {
6961 if ($maxoverride[$key]) {
6962 $this->get_instance()->{$key} = $maxoverride[$key];
6966 $formparams = array(
6967 'instance' => $this->get_instance(),
6968 'assign' => $this,
6969 'userlist' => $userlist
6972 $mform = new mod_assign_extension_form(null, $formparams);
6974 if ($mform->is_cancelled()) {
6975 return true;
6978 if ($formdata = $mform->get_data()) {
6979 if (!empty($formdata->selectedusers)) {
6980 $users = explode(',', $formdata->selectedusers);
6981 $result = true;
6982 foreach ($users as $userid) {
6983 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
6984 $result = $this->save_user_extension($user->id, $formdata->extensionduedate) && $result;
6986 return $result;
6988 if (!empty($formdata->userid)) {
6989 $user = $DB->get_record('user', array('id' => $formdata->userid), '*', MUST_EXIST);
6990 return $this->save_user_extension($user->id, $formdata->extensionduedate);
6994 return false;
6998 * Save quick grades.
7000 * @return string The result of the save operation
7002 protected function process_save_quick_grades() {
7003 global $USER, $DB, $CFG;
7005 // Need grade permission.
7006 require_capability('mod/assign:grade', $this->context);
7007 require_sesskey();
7009 // Make sure advanced grading is disabled.
7010 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
7011 $controller = $gradingmanager->get_active_controller();
7012 if (!empty($controller)) {
7013 $message = get_string('errorquickgradingvsadvancedgrading', 'assign');
7014 $this->set_error_message($message);
7015 return $message;
7018 $users = array();
7019 // First check all the last modified values.
7020 $currentgroup = groups_get_activity_group($this->get_course_module(), true);
7021 $participants = $this->list_participants($currentgroup, true);
7023 // Gets a list of possible users and look for values based upon that.
7024 foreach ($participants as $userid => $unused) {
7025 $modified = optional_param('grademodified_' . $userid, -1, PARAM_INT);
7026 $attemptnumber = optional_param('gradeattempt_' . $userid, -1, PARAM_INT);
7027 // Gather the userid, updated grade and last modified value.
7028 $record = new stdClass();
7029 $record->userid = $userid;
7030 if ($modified >= 0) {
7031 $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT));
7032 $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_ALPHA);
7033 $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT);
7034 } else {
7035 // This user was not in the grading table.
7036 continue;
7038 $record->attemptnumber = $attemptnumber;
7039 $record->lastmodified = $modified;
7040 $record->gradinginfo = grade_get_grades($this->get_course()->id,
7041 'mod',
7042 'assign',
7043 $this->get_instance()->id,
7044 array($userid));
7045 $users[$userid] = $record;
7048 if (empty($users)) {
7049 $message = get_string('nousersselected', 'assign');
7050 $this->set_error_message($message);
7051 return $message;
7054 list($userids, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED);
7055 $params['assignid1'] = $this->get_instance()->id;
7056 $params['assignid2'] = $this->get_instance()->id;
7058 // Check them all for currency.
7059 $grademaxattempt = 'SELECT s.userid, s.attemptnumber AS maxattempt
7060 FROM {assign_submission} s
7061 WHERE s.assignment = :assignid1 AND s.latest = 1';
7063 $sql = 'SELECT u.id AS userid, g.grade AS grade, g.timemodified AS lastmodified,
7064 uf.workflowstate, uf.allocatedmarker, gmx.maxattempt AS attemptnumber
7065 FROM {user} u
7066 LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
7067 LEFT JOIN {assign_grades} g ON
7068 u.id = g.userid AND
7069 g.assignment = :assignid2 AND
7070 g.attemptnumber = gmx.maxattempt
7071 LEFT JOIN {assign_user_flags} uf ON uf.assignment = g.assignment AND uf.userid = g.userid
7072 WHERE u.id ' . $userids;
7073 $currentgrades = $DB->get_recordset_sql($sql, $params);
7075 $modifiedusers = array();
7076 foreach ($currentgrades as $current) {
7077 $modified = $users[(int)$current->userid];
7078 $grade = $this->get_user_grade($modified->userid, false);
7079 // Check to see if the grade column was even visible.
7080 $gradecolpresent = optional_param('quickgrade_' . $modified->userid, false, PARAM_INT) !== false;
7082 // Check to see if the outcomes were modified.
7083 if ($CFG->enableoutcomes) {
7084 foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
7085 $oldoutcome = $outcome->grades[$modified->userid]->grade;
7086 $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
7087 $newoutcome = optional_param($paramname, -1, PARAM_FLOAT);
7088 // Check to see if the outcome column was even visible.
7089 $outcomecolpresent = optional_param($paramname, false, PARAM_FLOAT) !== false;
7090 if ($outcomecolpresent && ($oldoutcome != $newoutcome)) {
7091 // Can't check modified time for outcomes because it is not reported.
7092 $modifiedusers[$modified->userid] = $modified;
7093 continue;
7098 // Let plugins participate.
7099 foreach ($this->feedbackplugins as $plugin) {
7100 if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
7101 // The plugins must handle is_quickgrading_modified correctly - ie
7102 // handle hidden columns.
7103 if ($plugin->is_quickgrading_modified($modified->userid, $grade)) {
7104 if ((int)$current->lastmodified > (int)$modified->lastmodified) {
7105 $message = get_string('errorrecordmodified', 'assign');
7106 $this->set_error_message($message);
7107 return $message;
7108 } else {
7109 $modifiedusers[$modified->userid] = $modified;
7110 continue;
7116 if (($current->grade < 0 || $current->grade === null) &&
7117 ($modified->grade < 0 || $modified->grade === null)) {
7118 // Different ways to indicate no grade.
7119 $modified->grade = $current->grade; // Keep existing grade.
7121 // Treat 0 and null as different values.
7122 if ($current->grade !== null) {
7123 $current->grade = floatval($current->grade);
7125 $gradechanged = $gradecolpresent && grade_floats_different($current->grade, $modified->grade);
7126 $markingallocationchanged = $this->get_instance()->markingworkflow &&
7127 $this->get_instance()->markingallocation &&
7128 ($modified->allocatedmarker !== false) &&
7129 ($current->allocatedmarker != $modified->allocatedmarker);
7130 $workflowstatechanged = $this->get_instance()->markingworkflow &&
7131 ($modified->workflowstate !== false) &&
7132 ($current->workflowstate != $modified->workflowstate);
7133 if ($gradechanged || $markingallocationchanged || $workflowstatechanged) {
7134 // Grade changed.
7135 if ($this->grading_disabled($modified->userid)) {
7136 continue;
7138 $badmodified = (int)$current->lastmodified > (int)$modified->lastmodified;
7139 $badattempt = (int)$current->attemptnumber != (int)$modified->attemptnumber;
7140 if ($badmodified || $badattempt) {
7141 // Error - record has been modified since viewing the page.
7142 $message = get_string('errorrecordmodified', 'assign');
7143 $this->set_error_message($message);
7144 return $message;
7145 } else {
7146 $modifiedusers[$modified->userid] = $modified;
7151 $currentgrades->close();
7153 $adminconfig = $this->get_admin_config();
7154 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7156 // Ok - ready to process the updates.
7157 foreach ($modifiedusers as $userid => $modified) {
7158 $grade = $this->get_user_grade($userid, true);
7159 $flags = $this->get_user_flags($userid, true);
7160 $grade->grade= grade_floatval(unformat_float($modified->grade));
7161 $grade->grader= $USER->id;
7162 $gradecolpresent = optional_param('quickgrade_' . $userid, false, PARAM_INT) !== false;
7164 // Save plugins data.
7165 foreach ($this->feedbackplugins as $plugin) {
7166 if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
7167 $plugin->save_quickgrading_changes($userid, $grade);
7168 if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
7169 // This is the feedback plugin chose to push comments to the gradebook.
7170 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
7171 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
7172 $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
7177 // These will be set to false if they are not present in the quickgrading
7178 // form (e.g. column hidden).
7179 $workflowstatemodified = ($modified->workflowstate !== false) &&
7180 ($flags->workflowstate != $modified->workflowstate);
7182 $allocatedmarkermodified = ($modified->allocatedmarker !== false) &&
7183 ($flags->allocatedmarker != $modified->allocatedmarker);
7185 if ($workflowstatemodified) {
7186 $flags->workflowstate = $modified->workflowstate;
7188 if ($allocatedmarkermodified) {
7189 $flags->allocatedmarker = $modified->allocatedmarker;
7191 if ($workflowstatemodified || $allocatedmarkermodified) {
7192 if ($this->update_user_flags($flags) && $workflowstatemodified) {
7193 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
7194 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $flags->workflowstate)->trigger();
7197 $this->update_grade($grade);
7199 // Allow teachers to skip sending notifications.
7200 if (optional_param('sendstudentnotifications', true, PARAM_BOOL)) {
7201 $this->notify_grade_modified($grade, true);
7204 // Save outcomes.
7205 if ($CFG->enableoutcomes) {
7206 $data = array();
7207 foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
7208 $oldoutcome = $outcome->grades[$modified->userid]->grade;
7209 $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
7210 // This will be false if the input was not in the quickgrading
7211 // form (e.g. column hidden).
7212 $newoutcome = optional_param($paramname, false, PARAM_INT);
7213 if ($newoutcome !== false && ($oldoutcome != $newoutcome)) {
7214 $data[$outcomeid] = $newoutcome;
7217 if (count($data) > 0) {
7218 grade_update_outcomes('mod/assign',
7219 $this->course->id,
7220 'mod',
7221 'assign',
7222 $this->get_instance()->id,
7223 $userid,
7224 $data);
7229 return get_string('quickgradingchangessaved', 'assign');
7233 * Reveal student identities to markers (and the gradebook).
7235 * @return void
7237 public function reveal_identities() {
7238 global $DB;
7240 require_capability('mod/assign:revealidentities', $this->context);
7242 if ($this->get_instance()->revealidentities || empty($this->get_instance()->blindmarking)) {
7243 return false;
7246 // Update the assignment record.
7247 $update = new stdClass();
7248 $update->id = $this->get_instance()->id;
7249 $update->revealidentities = 1;
7250 $DB->update_record('assign', $update);
7252 // Refresh the instance data.
7253 $this->instance = null;
7255 // Release the grades to the gradebook.
7256 // First create the column in the gradebook.
7257 $this->update_gradebook(false, $this->get_course_module()->id);
7259 // Now release all grades.
7261 $adminconfig = $this->get_admin_config();
7262 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
7263 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
7264 $grades = $DB->get_records('assign_grades', array('assignment'=>$this->get_instance()->id));
7266 $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
7268 foreach ($grades as $grade) {
7269 // Fetch any comments for this student.
7270 if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
7271 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
7272 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
7273 $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
7275 $this->gradebook_item_update(null, $grade);
7278 \mod_assign\event\identities_revealed::create_from_assign($this)->trigger();
7282 * Reveal student identities to markers (and the gradebook).
7284 * @return void
7286 protected function process_reveal_identities() {
7288 if (!confirm_sesskey()) {
7289 return false;
7292 return $this->reveal_identities();
7297 * Save grading options.
7299 * @return void
7301 protected function process_save_grading_options() {
7302 global $USER, $CFG;
7304 // Include grading options form.
7305 require_once($CFG->dirroot . '/mod/assign/gradingoptionsform.php');
7307 // Need submit permission to submit an assignment.
7308 $this->require_view_grades();
7309 require_sesskey();
7311 // Is advanced grading enabled?
7312 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
7313 $controller = $gradingmanager->get_active_controller();
7314 $showquickgrading = empty($controller);
7315 if (!is_null($this->context)) {
7316 $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
7317 } else {
7318 $showonlyactiveenrolopt = false;
7321 $markingallocation = $this->get_instance()->markingworkflow &&
7322 $this->get_instance()->markingallocation &&
7323 has_capability('mod/assign:manageallocations', $this->context);
7324 // Get markers to use in drop lists.
7325 $markingallocationoptions = array();
7326 if ($markingallocation) {
7327 $markingallocationoptions[''] = get_string('filternone', 'assign');
7328 $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
7329 list($sort, $params) = users_order_by_sql('u');
7330 // Only enrolled users could be assigned as potential markers.
7331 $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
7332 foreach ($markers as $marker) {
7333 $markingallocationoptions[$marker->id] = fullname($marker);
7337 // Get marking states to show in form.
7338 $markingworkflowoptions = $this->get_marking_workflow_filters();
7340 $gradingoptionsparams = array('cm'=>$this->get_course_module()->id,
7341 'contextid'=>$this->context->id,
7342 'userid'=>$USER->id,
7343 'submissionsenabled'=>$this->is_any_submission_plugin_enabled(),
7344 'showquickgrading'=>$showquickgrading,
7345 'quickgrading'=>false,
7346 'markingworkflowopt' => $markingworkflowoptions,
7347 'markingallocationopt' => $markingallocationoptions,
7348 'showonlyactiveenrolopt'=>$showonlyactiveenrolopt,
7349 'showonlyactiveenrol' => $this->show_only_active_users(),
7350 'downloadasfolders' => get_user_preferences('assign_downloadasfolders', 1));
7351 $mform = new mod_assign_grading_options_form(null, $gradingoptionsparams);
7352 if ($formdata = $mform->get_data()) {
7353 set_user_preference('assign_perpage', $formdata->perpage);
7354 if (isset($formdata->filter)) {
7355 set_user_preference('assign_filter', $formdata->filter);
7357 if (isset($formdata->markerfilter)) {
7358 set_user_preference('assign_markerfilter', $formdata->markerfilter);
7360 if (isset($formdata->workflowfilter)) {
7361 set_user_preference('assign_workflowfilter', $formdata->workflowfilter);
7363 if ($showquickgrading) {
7364 set_user_preference('assign_quickgrading', isset($formdata->quickgrading));
7366 if (isset($formdata->downloadasfolders)) {
7367 set_user_preference('assign_downloadasfolders', 1); // Enabled.
7368 } else {
7369 set_user_preference('assign_downloadasfolders', 0); // Disabled.
7371 if (!empty($showonlyactiveenrolopt)) {
7372 $showonlyactiveenrol = isset($formdata->showonlyactiveenrol);
7373 set_user_preference('grade_report_showonlyactiveenrol', $showonlyactiveenrol);
7374 $this->showonlyactiveenrol = $showonlyactiveenrol;
7380 * Take a grade object and print a short summary for the log file.
7381 * The size limit for the log file is 255 characters, so be careful not
7382 * to include too much information.
7384 * @deprecated since 2.7
7386 * @param stdClass $grade
7387 * @return string
7389 public function format_grade_for_log(stdClass $grade) {
7390 global $DB;
7392 $user = $DB->get_record('user', array('id' => $grade->userid), '*', MUST_EXIST);
7394 $info = get_string('gradestudent', 'assign', array('id'=>$user->id, 'fullname'=>fullname($user)));
7395 if ($grade->grade != '') {
7396 $info .= get_string('gradenoun') . ': ' . $this->display_grade($grade->grade, false) . '. ';
7397 } else {
7398 $info .= get_string('nograde', 'assign');
7400 return $info;
7404 * Take a submission object and print a short summary for the log file.
7405 * The size limit for the log file is 255 characters, so be careful not
7406 * to include too much information.
7408 * @deprecated since 2.7
7410 * @param stdClass $submission
7411 * @return string
7413 public function format_submission_for_log(stdClass $submission) {
7414 global $DB;
7416 $info = '';
7417 if ($submission->userid) {
7418 $user = $DB->get_record('user', array('id' => $submission->userid), '*', MUST_EXIST);
7419 $name = fullname($user);
7420 } else {
7421 $group = $this->get_submission_group($submission->userid);
7422 if ($group) {
7423 $name = $group->name;
7424 } else {
7425 $name = get_string('defaultteam', 'assign');
7428 $status = get_string('submissionstatus_' . $submission->status, 'assign');
7429 $params = array('id'=>$submission->userid, 'fullname'=>$name, 'status'=>$status);
7430 $info .= get_string('submissionlog', 'assign', $params) . ' <br>';
7432 foreach ($this->submissionplugins as $plugin) {
7433 if ($plugin->is_enabled() && $plugin->is_visible()) {
7434 $info .= '<br>' . $plugin->format_for_log($submission);
7438 return $info;
7442 * Require a valid sess key and then call copy_previous_attempt.
7444 * @param array $notices Any error messages that should be shown
7445 * to the user at the top of the edit submission form.
7446 * @return bool
7448 protected function process_copy_previous_attempt(&$notices) {
7449 require_sesskey();
7451 return $this->copy_previous_attempt($notices);
7455 * Copy the current assignment submission from the last submitted attempt.
7457 * @param array $notices Any error messages that should be shown
7458 * to the user at the top of the edit submission form.
7459 * @return bool
7461 public function copy_previous_attempt(&$notices) {
7462 global $USER, $CFG;
7464 require_capability('mod/assign:submit', $this->context);
7466 $instance = $this->get_instance();
7467 if ($instance->teamsubmission) {
7468 $submission = $this->get_group_submission($USER->id, 0, true);
7469 } else {
7470 $submission = $this->get_user_submission($USER->id, true);
7472 if (!$submission || $submission->status != ASSIGN_SUBMISSION_STATUS_REOPENED) {
7473 $notices[] = get_string('submissionnotcopiedinvalidstatus', 'assign');
7474 return false;
7476 $flags = $this->get_user_flags($USER->id, false);
7478 // Get the flags to check if it is locked.
7479 if ($flags && $flags->locked) {
7480 $notices[] = get_string('submissionslocked', 'assign');
7481 return false;
7483 if ($instance->submissiondrafts) {
7484 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
7485 } else {
7486 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
7488 $this->update_submission($submission, $USER->id, true, $instance->teamsubmission);
7490 // Find the previous submission.
7491 if ($instance->teamsubmission) {
7492 $previoussubmission = $this->get_group_submission($USER->id, 0, true, $submission->attemptnumber - 1);
7493 } else {
7494 $previoussubmission = $this->get_user_submission($USER->id, true, $submission->attemptnumber - 1);
7497 if (!$previoussubmission) {
7498 // There was no previous submission so there is nothing else to do.
7499 return true;
7502 $pluginerror = false;
7503 foreach ($this->get_submission_plugins() as $plugin) {
7504 if ($plugin->is_visible() && $plugin->is_enabled()) {
7505 if (!$plugin->copy_submission($previoussubmission, $submission)) {
7506 $notices[] = $plugin->get_error();
7507 $pluginerror = true;
7511 if ($pluginerror) {
7512 return false;
7515 \mod_assign\event\submission_duplicated::create_from_submission($this, $submission)->trigger();
7517 $complete = COMPLETION_INCOMPLETE;
7518 if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
7519 $complete = COMPLETION_COMPLETE;
7521 $completion = new completion_info($this->get_course());
7522 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
7523 $this->update_activity_completion_records($instance->teamsubmission,
7524 $instance->requireallteammemberssubmit,
7525 $submission,
7526 $USER->id,
7527 $complete,
7528 $completion);
7531 if (!$instance->submissiondrafts) {
7532 // There is a case for not notifying the student about the submission copy,
7533 // but it provides a record of the event and if they then cancel editing it
7534 // is clear that the submission was copied.
7535 $this->notify_student_submission_copied($submission);
7536 $this->notify_graders($submission);
7538 // The same logic applies here - we could not notify teachers,
7539 // but then they would wonder why there are submitted assignments
7540 // and they haven't been notified.
7541 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
7543 return true;
7547 * Determine if the current submission is empty or not.
7549 * @param submission $submission the students submission record to check.
7550 * @return bool
7552 public function submission_empty($submission) {
7553 $allempty = true;
7555 foreach ($this->submissionplugins as $plugin) {
7556 if ($plugin->is_enabled() && $plugin->is_visible()) {
7557 if (!$allempty || !$plugin->is_empty($submission)) {
7558 $allempty = false;
7562 return $allempty;
7566 * Determine if a new submission is empty or not
7568 * @param stdClass $data Submission data
7569 * @return bool
7571 public function new_submission_empty($data) {
7572 foreach ($this->submissionplugins as $plugin) {
7573 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions() &&
7574 !$plugin->submission_is_empty($data)) {
7575 return false;
7578 return true;
7582 * Save assignment submission for the current user.
7584 * @param stdClass $data
7585 * @param array $notices Any error messages that should be shown
7586 * to the user.
7587 * @return bool
7589 public function save_submission(stdClass $data, & $notices) {
7590 global $CFG, $USER, $DB;
7592 $userid = $USER->id;
7593 if (!empty($data->userid)) {
7594 $userid = $data->userid;
7597 $user = clone($USER);
7598 if ($userid == $USER->id) {
7599 require_capability('mod/assign:submit', $this->context);
7600 } else {
7601 $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
7602 if (!$this->can_edit_submission($userid, $USER->id)) {
7603 throw new \moodle_exception('nopermission');
7606 $instance = $this->get_instance();
7608 if ($instance->teamsubmission) {
7609 $submission = $this->get_group_submission($userid, 0, true);
7610 } else {
7611 $submission = $this->get_user_submission($userid, true);
7614 if ($this->new_submission_empty($data)) {
7615 $notices[] = get_string('submissionempty', 'mod_assign');
7616 return false;
7619 // Check that no one has modified the submission since we started looking at it.
7620 if (isset($data->lastmodified) && ($submission->timemodified > $data->lastmodified)) {
7621 // Another user has submitted something. Notify the current user.
7622 if ($submission->status !== ASSIGN_SUBMISSION_STATUS_NEW) {
7623 $notices[] = $instance->teamsubmission ? get_string('submissionmodifiedgroup', 'mod_assign')
7624 : get_string('submissionmodified', 'mod_assign');
7625 return false;
7629 if ($instance->submissiondrafts) {
7630 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
7631 } else {
7632 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
7635 $flags = $this->get_user_flags($userid, false);
7637 // Get the flags to check if it is locked.
7638 if ($flags && $flags->locked) {
7639 throw new \moodle_exception('submissionslocked', 'assign');
7640 return true;
7643 $pluginerror = false;
7644 foreach ($this->submissionplugins as $plugin) {
7645 if ($plugin->is_enabled() && $plugin->is_visible()) {
7646 if (!$plugin->save($submission, $data)) {
7647 $notices[] = $plugin->get_error();
7648 $pluginerror = true;
7653 $allempty = $this->submission_empty($submission);
7654 if ($pluginerror || $allempty) {
7655 if ($allempty) {
7656 $notices[] = get_string('submissionempty', 'mod_assign');
7658 return false;
7661 $this->update_submission($submission, $userid, true, $instance->teamsubmission);
7662 $users = [$userid];
7664 if ($instance->teamsubmission && !$instance->requireallteammemberssubmit) {
7665 $team = $this->get_submission_group_members($submission->groupid, true);
7667 foreach ($team as $member) {
7668 if ($member->id != $userid) {
7669 $membersubmission = clone($submission);
7670 $this->update_submission($membersubmission, $member->id, true, $instance->teamsubmission);
7671 $users[] = $member->id;
7676 $complete = COMPLETION_INCOMPLETE;
7677 if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
7678 $complete = COMPLETION_COMPLETE;
7681 $completion = new completion_info($this->get_course());
7682 if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
7683 foreach ($users as $id) {
7684 $completion->update_state($this->get_course_module(), $complete, $id);
7688 // Logging.
7689 if (isset($data->submissionstatement) && ($userid == $USER->id)) {
7690 \mod_assign\event\statement_accepted::create_from_submission($this, $submission)->trigger();
7693 if (!$instance->submissiondrafts) {
7694 $this->notify_student_submission_receipt($submission);
7695 $this->notify_graders($submission);
7696 \mod_assign\event\assessable_submitted::create_from_submission($this, $submission, true)->trigger();
7698 return true;
7702 * Save assignment submission.
7704 * @param moodleform $mform
7705 * @param array $notices Any error messages that should be shown
7706 * to the user at the top of the edit submission form.
7707 * @return bool
7709 protected function process_save_submission(&$mform, &$notices) {
7710 global $CFG, $USER;
7712 // Include submission form.
7713 require_once($CFG->dirroot . '/mod/assign/submission_form.php');
7715 $userid = optional_param('userid', $USER->id, PARAM_INT);
7716 // Need submit permission to submit an assignment.
7717 require_sesskey();
7718 if (!$this->submissions_open($userid)) {
7719 $notices[] = get_string('duedatereached', 'assign');
7720 return false;
7722 $instance = $this->get_instance();
7724 $data = new stdClass();
7725 $data->userid = $userid;
7726 $mform = new mod_assign_submission_form(null, array($this, $data));
7727 if ($mform->is_cancelled()) {
7728 return true;
7730 if ($data = $mform->get_data()) {
7731 return $this->save_submission($data, $notices);
7733 return false;
7738 * Determine if this users grade can be edited.
7740 * @param int $userid - The student userid
7741 * @param bool $checkworkflow - whether to include a check for the workflow state.
7742 * @param stdClass $gradinginfo - optional, allow gradinginfo to be passed for performance.
7743 * @return bool $gradingdisabled
7745 public function grading_disabled($userid, $checkworkflow = true, $gradinginfo = null) {
7746 if ($checkworkflow && $this->get_instance()->markingworkflow) {
7747 $grade = $this->get_user_grade($userid, false);
7748 $validstates = $this->get_marking_workflow_states_for_current_user();
7749 if (!empty($grade) && !empty($grade->workflowstate) && !array_key_exists($grade->workflowstate, $validstates)) {
7750 return true;
7754 if (is_null($gradinginfo)) {
7755 $gradinginfo = grade_get_grades($this->get_course()->id,
7756 'mod',
7757 'assign',
7758 $this->get_instance()->id,
7759 array($userid));
7762 if (!$gradinginfo) {
7763 return false;
7766 if (!isset($gradinginfo->items[0]->grades[$userid])) {
7767 return false;
7769 $gradingdisabled = $gradinginfo->items[0]->grades[$userid]->locked ||
7770 $gradinginfo->items[0]->grades[$userid]->overridden;
7771 return $gradingdisabled;
7776 * Get an instance of a grading form if advanced grading is enabled.
7777 * This is specific to the assignment, marker and student.
7779 * @param int $userid - The student userid
7780 * @param stdClass|false $grade - The grade record
7781 * @param bool $gradingdisabled
7782 * @return mixed gradingform_instance|null $gradinginstance
7784 protected function get_grading_instance($userid, $grade, $gradingdisabled) {
7785 global $CFG, $USER;
7787 $grademenu = make_grades_menu($this->get_instance()->grade);
7788 $allowgradedecimals = $this->get_instance()->grade > 0;
7790 $advancedgradingwarning = false;
7791 $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions');
7792 $gradinginstance = null;
7793 if ($gradingmethod = $gradingmanager->get_active_method()) {
7794 $controller = $gradingmanager->get_controller($gradingmethod);
7795 if ($controller->is_form_available()) {
7796 $itemid = null;
7797 if ($grade) {
7798 $itemid = $grade->id;
7800 if ($gradingdisabled && $itemid) {
7801 $gradinginstance = $controller->get_current_instance($USER->id, $itemid);
7802 } else if (!$gradingdisabled) {
7803 $instanceid = optional_param('advancedgradinginstanceid', 0, PARAM_INT);
7804 $gradinginstance = $controller->get_or_create_instance($instanceid,
7805 $USER->id,
7806 $itemid);
7808 } else {
7809 $advancedgradingwarning = $controller->form_unavailable_notification();
7812 if ($gradinginstance) {
7813 $gradinginstance->get_controller()->set_grade_range($grademenu, $allowgradedecimals);
7815 return $gradinginstance;
7819 * Add elements to grade form.
7821 * @param MoodleQuickForm $mform
7822 * @param stdClass $data
7823 * @param array $params
7824 * @return void
7826 public function add_grade_form_elements(MoodleQuickForm $mform, stdClass $data, $params) {
7827 global $USER, $CFG, $SESSION;
7828 $settings = $this->get_instance();
7830 $rownum = isset($params['rownum']) ? $params['rownum'] : 0;
7831 $last = isset($params['last']) ? $params['last'] : true;
7832 $useridlistid = isset($params['useridlistid']) ? $params['useridlistid'] : 0;
7833 $userid = isset($params['userid']) ? $params['userid'] : 0;
7834 $attemptnumber = isset($params['attemptnumber']) ? $params['attemptnumber'] : 0;
7835 $gradingpanel = !empty($params['gradingpanel']);
7836 $bothids = ($userid && $useridlistid);
7838 if (!$userid || $bothids) {
7839 $useridlist = $this->get_grading_userid_list(true, $useridlistid);
7840 } else {
7841 $useridlist = array($userid);
7842 $rownum = 0;
7843 $useridlistid = '';
7846 $userid = $useridlist[$rownum];
7847 // We need to create a grade record matching this attempt number
7848 // or the feedback plugin will have no way to know what is the correct attempt.
7849 $grade = $this->get_user_grade($userid, true, $attemptnumber);
7851 $submission = null;
7852 if ($this->get_instance()->teamsubmission) {
7853 $submission = $this->get_group_submission($userid, 0, false, $attemptnumber);
7854 } else {
7855 $submission = $this->get_user_submission($userid, false, $attemptnumber);
7858 // Add advanced grading.
7859 $gradingdisabled = $this->grading_disabled($userid);
7860 $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
7862 $mform->addElement('header', 'gradeheader', get_string('gradenoun'));
7863 if ($gradinginstance) {
7864 $gradingelement = $mform->addElement('grading',
7865 'advancedgrading',
7866 get_string('gradenoun') . ':',
7867 array('gradinginstance' => $gradinginstance));
7868 if ($gradingdisabled) {
7869 $gradingelement->freeze();
7870 } else {
7871 $mform->addElement('hidden', 'advancedgradinginstanceid', $gradinginstance->get_id());
7872 $mform->setType('advancedgradinginstanceid', PARAM_INT);
7874 } else {
7875 // Use simple direct grading.
7876 if ($this->get_instance()->grade > 0) {
7877 $name = get_string('gradeoutof', 'assign', $this->get_instance()->grade);
7878 if (!$gradingdisabled) {
7879 $gradingelement = $mform->addElement('text', 'grade', $name);
7880 $mform->addHelpButton('grade', 'gradeoutofhelp', 'assign');
7881 $mform->setType('grade', PARAM_RAW);
7882 } else {
7883 $strgradelocked = get_string('gradelocked', 'assign');
7884 $mform->addElement('static', 'gradedisabled', $name, $strgradelocked);
7885 $mform->addHelpButton('gradedisabled', 'gradeoutofhelp', 'assign');
7887 } else {
7888 $grademenu = array(-1 => get_string("nograde")) + make_grades_menu($this->get_instance()->grade);
7889 if (count($grademenu) > 1) {
7890 $gradingelement = $mform->addElement('select', 'grade', get_string('gradenoun') . ':', $grademenu);
7892 // The grade is already formatted with format_float so it needs to be converted back to an integer.
7893 if (!empty($data->grade)) {
7894 $data->grade = (int)unformat_float($data->grade);
7896 $mform->setType('grade', PARAM_INT);
7897 if ($gradingdisabled) {
7898 $gradingelement->freeze();
7904 $gradinginfo = grade_get_grades($this->get_course()->id,
7905 'mod',
7906 'assign',
7907 $this->get_instance()->id,
7908 $userid);
7909 if (!empty($CFG->enableoutcomes)) {
7910 foreach ($gradinginfo->outcomes as $index => $outcome) {
7911 $options = make_grades_menu(-$outcome->scaleid);
7912 $options[0] = get_string('nooutcome', 'grades');
7913 if ($outcome->grades[$userid]->locked) {
7914 $mform->addElement('static',
7915 'outcome_' . $index . '[' . $userid . ']',
7916 $outcome->name . ':',
7917 $options[$outcome->grades[$userid]->grade]);
7918 } else {
7919 $attributes = array('id' => 'menuoutcome_' . $index );
7920 $mform->addElement('select',
7921 'outcome_' . $index . '[' . $userid . ']',
7922 $outcome->name.':',
7923 $options,
7924 $attributes);
7925 $mform->setType('outcome_' . $index . '[' . $userid . ']', PARAM_INT);
7926 $mform->setDefault('outcome_' . $index . '[' . $userid . ']',
7927 $outcome->grades[$userid]->grade);
7932 $capabilitylist = array('gradereport/grader:view', 'moodle/grade:viewall');
7933 $usergrade = get_string('notgraded', 'assign');
7934 if (has_all_capabilities($capabilitylist, $this->get_course_context())) {
7935 $urlparams = array('id'=>$this->get_course()->id);
7936 $url = new moodle_url('/grade/report/grader/index.php', $urlparams);
7937 if (isset($gradinginfo->items[0]->grades[$userid]->grade)) {
7938 $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
7940 $gradestring = $this->get_renderer()->action_link($url, $usergrade);
7941 } else {
7942 if (isset($gradinginfo->items[0]->grades[$userid]) &&
7943 !$gradinginfo->items[0]->grades[$userid]->hidden) {
7944 $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
7946 $gradestring = $usergrade;
7949 if ($this->get_instance()->markingworkflow) {
7950 $states = $this->get_marking_workflow_states_for_current_user();
7951 $options = array('' => get_string('markingworkflowstatenotmarked', 'assign')) + $states;
7952 $mform->addElement('select', 'workflowstate', get_string('markingworkflowstate', 'assign'), $options);
7953 $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign');
7954 $gradingstatus = $this->get_grading_status($userid);
7955 if ($gradingstatus != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
7956 if ($grade->grade && $grade->grade != -1) {
7957 $assigngradestring = html_writer::span(
7958 make_grades_menu($settings->grade)[grade_floatval($grade->grade)], 'currentgrade'
7960 $label = get_string('currentassigngrade', 'assign');
7961 $mform->addElement('static', 'currentassigngrade', $label, $assigngradestring);
7966 if ($this->get_instance()->markingworkflow &&
7967 $this->get_instance()->markingallocation &&
7968 has_capability('mod/assign:manageallocations', $this->context)) {
7970 list($sort, $params) = users_order_by_sql('u');
7971 // Only enrolled users could be assigned as potential markers.
7972 $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
7973 $markerlist = array('' => get_string('choosemarker', 'assign'));
7974 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
7975 foreach ($markers as $marker) {
7976 $markerlist[$marker->id] = fullname($marker, $viewfullnames);
7978 $mform->addElement('select', 'allocatedmarker', get_string('allocatedmarker', 'assign'), $markerlist);
7979 $mform->addHelpButton('allocatedmarker', 'allocatedmarker', 'assign');
7980 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW);
7981 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW);
7982 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE);
7983 $mform->disabledIf('allocatedmarker', 'workflowstate', 'eq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
7986 $gradestring = '<span class="currentgrade">' . $gradestring . '</span>';
7987 $mform->addElement('static', 'currentgrade', get_string('currentgrade', 'assign'), $gradestring);
7989 if (count($useridlist) > 1) {
7990 $strparams = array('current'=>$rownum+1, 'total'=>count($useridlist));
7991 $name = get_string('outof', 'assign', $strparams);
7992 $mform->addElement('static', 'gradingstudent', get_string('gradingstudent', 'assign'), $name);
7995 // Let feedback plugins add elements to the grading form.
7996 $this->add_plugin_grade_elements($grade, $mform, $data, $userid);
7998 // Hidden params.
7999 $mform->addElement('hidden', 'id', $this->get_course_module()->id);
8000 $mform->setType('id', PARAM_INT);
8001 $mform->addElement('hidden', 'rownum', $rownum);
8002 $mform->setType('rownum', PARAM_INT);
8003 $mform->setConstant('rownum', $rownum);
8004 $mform->addElement('hidden', 'useridlistid', $useridlistid);
8005 $mform->setType('useridlistid', PARAM_ALPHANUM);
8006 $mform->addElement('hidden', 'attemptnumber', $attemptnumber);
8007 $mform->setType('attemptnumber', PARAM_INT);
8008 $mform->addElement('hidden', 'ajax', optional_param('ajax', 0, PARAM_INT));
8009 $mform->setType('ajax', PARAM_INT);
8010 $mform->addElement('hidden', 'userid', optional_param('userid', 0, PARAM_INT));
8011 $mform->setType('userid', PARAM_INT);
8013 if ($this->get_instance()->teamsubmission) {
8014 $mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign'));
8015 $mform->addElement('selectyesno', 'applytoall', get_string('applytoteam', 'assign'));
8016 $mform->setDefault('applytoall', 1);
8019 // Do not show if we are editing a previous attempt.
8020 if (($attemptnumber == -1 ||
8021 ($attemptnumber + 1) == count($this->get_all_submissions($userid))) &&
8022 $this->get_instance()->attemptreopenmethod != ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
8023 $mform->addElement('header', 'attemptsettings', get_string('attemptsettings', 'assign'));
8024 $attemptreopenmethod = get_string('attemptreopenmethod_' . $this->get_instance()->attemptreopenmethod, 'assign');
8025 $mform->addElement('static', 'attemptreopenmethod', get_string('attemptreopenmethod', 'assign'), $attemptreopenmethod);
8027 $attemptnumber = 0;
8028 if ($submission) {
8029 $attemptnumber = $submission->attemptnumber;
8031 $maxattempts = $this->get_instance()->maxattempts;
8032 if ($maxattempts == ASSIGN_UNLIMITED_ATTEMPTS) {
8033 $maxattempts = get_string('unlimitedattempts', 'assign');
8035 $mform->addelement('static', 'maxattemptslabel', get_string('maxattempts', 'assign'), $maxattempts);
8036 $mform->addelement('static', 'attemptnumberlabel', get_string('attemptnumber', 'assign'), $attemptnumber + 1);
8038 $ismanual = $this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL;
8039 $issubmission = !empty($submission);
8040 $isunlimited = $this->get_instance()->maxattempts == ASSIGN_UNLIMITED_ATTEMPTS;
8041 $islessthanmaxattempts = $issubmission && ($submission->attemptnumber < ($this->get_instance()->maxattempts-1));
8043 if ($ismanual && (!$issubmission || $isunlimited || $islessthanmaxattempts)) {
8044 $mform->addElement('selectyesno', 'addattempt', get_string('addattempt', 'assign'));
8045 $mform->setDefault('addattempt', 0);
8048 if (!$gradingpanel) {
8049 $mform->addElement('selectyesno', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
8050 } else {
8051 $mform->addElement('hidden', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
8052 $mform->setType('sendstudentnotifications', PARAM_BOOL);
8054 // Get assignment visibility information for student.
8055 $modinfo = get_fast_modinfo($settings->course, $userid);
8056 $cm = $modinfo->get_cm($this->get_course_module()->id);
8058 // Don't allow notification to be sent if the student can't access the assignment,
8059 // or until in "Released" state if using marking workflow.
8060 if (!$cm->uservisible) {
8061 $mform->setDefault('sendstudentnotifications', 0);
8062 $mform->freeze('sendstudentnotifications');
8063 } else if ($this->get_instance()->markingworkflow) {
8064 $mform->setDefault('sendstudentnotifications', 0);
8065 if (!$gradingpanel) {
8066 $mform->disabledIf('sendstudentnotifications', 'workflowstate', 'neq', ASSIGN_MARKING_WORKFLOW_STATE_RELEASED);
8068 } else {
8069 $mform->setDefault('sendstudentnotifications', $this->get_instance()->sendstudentnotifications);
8072 $mform->addElement('hidden', 'action', 'submitgrade');
8073 $mform->setType('action', PARAM_ALPHA);
8075 if (!$gradingpanel) {
8077 $buttonarray = array();
8078 $name = get_string('savechanges', 'assign');
8079 $buttonarray[] = $mform->createElement('submit', 'savegrade', $name);
8080 if (!$last) {
8081 $name = get_string('savenext', 'assign');
8082 $buttonarray[] = $mform->createElement('submit', 'saveandshownext', $name);
8084 $buttonarray[] = $mform->createElement('cancel', 'cancelbutton', get_string('cancel'));
8085 $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
8086 $mform->closeHeaderBefore('buttonar');
8087 $buttonarray = array();
8089 if ($rownum > 0) {
8090 $name = get_string('previous', 'assign');
8091 $buttonarray[] = $mform->createElement('submit', 'nosaveandprevious', $name);
8094 if (!$last) {
8095 $name = get_string('nosavebutnext', 'assign');
8096 $buttonarray[] = $mform->createElement('submit', 'nosaveandnext', $name);
8098 if (!empty($buttonarray)) {
8099 $mform->addGroup($buttonarray, 'navar', '', array(' '), false);
8102 // The grading form does not work well with shortforms.
8103 $mform->setDisableShortforms();
8107 * Add elements in submission plugin form.
8109 * @param mixed $submission stdClass|null
8110 * @param MoodleQuickForm $mform
8111 * @param stdClass $data
8112 * @param int $userid The current userid (same as $USER->id)
8113 * @return void
8115 protected function add_plugin_submission_elements($submission,
8116 MoodleQuickForm $mform,
8117 stdClass $data,
8118 $userid) {
8119 foreach ($this->submissionplugins as $plugin) {
8120 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
8121 $plugin->get_form_elements_for_user($submission, $mform, $data, $userid);
8127 * Check if feedback plugins installed are enabled.
8129 * @return bool
8131 public function is_any_feedback_plugin_enabled() {
8132 if (!isset($this->cache['any_feedback_plugin_enabled'])) {
8133 $this->cache['any_feedback_plugin_enabled'] = false;
8134 foreach ($this->feedbackplugins as $plugin) {
8135 if ($plugin->is_enabled() && $plugin->is_visible()) {
8136 $this->cache['any_feedback_plugin_enabled'] = true;
8137 break;
8142 return $this->cache['any_feedback_plugin_enabled'];
8147 * Check if submission plugins installed are enabled.
8149 * @return bool
8151 public function is_any_submission_plugin_enabled() {
8152 if (!isset($this->cache['any_submission_plugin_enabled'])) {
8153 $this->cache['any_submission_plugin_enabled'] = false;
8154 foreach ($this->submissionplugins as $plugin) {
8155 if ($plugin->is_enabled() && $plugin->is_visible() && $plugin->allow_submissions()) {
8156 $this->cache['any_submission_plugin_enabled'] = true;
8157 break;
8162 return $this->cache['any_submission_plugin_enabled'];
8167 * Add elements to submission form.
8168 * @param MoodleQuickForm $mform
8169 * @param stdClass $data
8170 * @return void
8172 public function add_submission_form_elements(MoodleQuickForm $mform, stdClass $data) {
8173 global $USER;
8175 $userid = $data->userid;
8176 // Team submissions.
8177 if ($this->get_instance()->teamsubmission) {
8178 $submission = $this->get_group_submission($userid, 0, false);
8179 } else {
8180 $submission = $this->get_user_submission($userid, false);
8183 // Submission statement.
8184 $adminconfig = $this->get_admin_config();
8185 $requiresubmissionstatement = $this->get_instance()->requiresubmissionstatement;
8187 $draftsenabled = $this->get_instance()->submissiondrafts;
8188 $submissionstatement = '';
8190 if ($requiresubmissionstatement) {
8191 $submissionstatement = $this->get_submissionstatement($adminconfig, $this->get_instance(), $this->get_context());
8194 // If we get back an empty submission statement, we have to set $requiredsubmisisonstatement to false to prevent
8195 // that the submission statement checkbox will be displayed.
8196 if (empty($submissionstatement)) {
8197 $requiresubmissionstatement = false;
8200 $mform->addElement('header', 'submission header', get_string('addsubmission', 'mod_assign'));
8202 // Only show submission statement if we are editing our own submission.
8203 if ($requiresubmissionstatement && !$draftsenabled && $userid == $USER->id) {
8204 $mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement);
8205 $mform->addRule('submissionstatement', get_string('required'), 'required', null, 'client');
8208 $this->add_plugin_submission_elements($submission, $mform, $data, $userid);
8210 // Hidden params.
8211 $mform->addElement('hidden', 'id', $this->get_course_module()->id);
8212 $mform->setType('id', PARAM_INT);
8214 $mform->addElement('hidden', 'userid', $userid);
8215 $mform->setType('userid', PARAM_INT);
8217 $mform->addElement('hidden', 'action', 'savesubmission');
8218 $mform->setType('action', PARAM_ALPHA);
8222 * Remove any data from the current submission.
8224 * @param int $userid
8225 * @return boolean
8226 * @throws coding_exception
8228 public function remove_submission($userid) {
8229 global $USER;
8231 if (!$this->can_edit_submission($userid, $USER->id)) {
8232 $user = core_user::get_user($userid);
8233 $message = get_string('usersubmissioncannotberemoved', 'assign', fullname($user));
8234 $this->set_error_message($message);
8235 return false;
8238 if ($this->get_instance()->teamsubmission) {
8239 $submission = $this->get_group_submission($userid, 0, false);
8240 } else {
8241 $submission = $this->get_user_submission($userid, false);
8244 if (!$submission) {
8245 return false;
8247 $submission->status = $submission->attemptnumber ? ASSIGN_SUBMISSION_STATUS_REOPENED : ASSIGN_SUBMISSION_STATUS_NEW;
8248 $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
8250 // Tell each submission plugin we were saved with no data.
8251 $plugins = $this->get_submission_plugins();
8252 foreach ($plugins as $plugin) {
8253 if ($plugin->is_enabled() && $plugin->is_visible()) {
8254 $plugin->remove($submission);
8258 $completion = new completion_info($this->get_course());
8259 if ($completion->is_enabled($this->get_course_module()) &&
8260 $this->get_instance()->completionsubmit) {
8261 $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
8264 submission_removed::create_from_submission($this, $submission)->trigger();
8265 submission_status_updated::create_from_submission($this, $submission)->trigger();
8266 return true;
8270 * Revert to draft.
8272 * @param int $userid
8273 * @return boolean
8275 public function revert_to_draft($userid) {
8276 global $DB, $USER;
8278 // Need grade permission.
8279 require_capability('mod/assign:grade', $this->context);
8281 if ($this->get_instance()->teamsubmission) {
8282 $submission = $this->get_group_submission($userid, 0, false);
8283 } else {
8284 $submission = $this->get_user_submission($userid, false);
8287 if (!$submission) {
8288 return false;
8290 $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
8291 $this->update_submission($submission, $userid, false, $this->get_instance()->teamsubmission);
8293 // Give each submission plugin a chance to process the reverting to draft.
8294 $plugins = $this->get_submission_plugins();
8295 foreach ($plugins as $plugin) {
8296 if ($plugin->is_enabled() && $plugin->is_visible()) {
8297 $plugin->revert_to_draft($submission);
8300 // Update the modified time on the grade (grader modified).
8301 $grade = $this->get_user_grade($userid, true);
8302 $grade->grader = $USER->id;
8303 $this->update_grade($grade);
8305 $completion = new completion_info($this->get_course());
8306 if ($completion->is_enabled($this->get_course_module()) &&
8307 $this->get_instance()->completionsubmit) {
8308 $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
8310 \mod_assign\event\submission_status_updated::create_from_submission($this, $submission)->trigger();
8311 return true;
8315 * Remove the current submission.
8317 * @param int $userid
8318 * @return boolean
8320 protected function process_remove_submission($userid = 0) {
8321 require_sesskey();
8323 if (!$userid) {
8324 $userid = required_param('userid', PARAM_INT);
8327 return $this->remove_submission($userid);
8331 * Revert to draft.
8332 * Uses url parameter userid if userid not supplied as a parameter.
8334 * @param int $userid
8335 * @return boolean
8337 protected function process_revert_to_draft($userid = 0) {
8338 require_sesskey();
8340 if (!$userid) {
8341 $userid = required_param('userid', PARAM_INT);
8344 return $this->revert_to_draft($userid);
8348 * Prevent student updates to this submission
8350 * @param int $userid
8351 * @return bool
8353 public function lock_submission($userid) {
8354 global $USER, $DB;
8355 // Need grade permission.
8356 require_capability('mod/assign:grade', $this->context);
8358 // Give each submission plugin a chance to process the locking.
8359 $plugins = $this->get_submission_plugins();
8360 $submission = $this->get_user_submission($userid, false);
8362 $flags = $this->get_user_flags($userid, true);
8363 $flags->locked = 1;
8364 $this->update_user_flags($flags);
8366 foreach ($plugins as $plugin) {
8367 if ($plugin->is_enabled() && $plugin->is_visible()) {
8368 $plugin->lock($submission, $flags);
8372 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8373 \mod_assign\event\submission_locked::create_from_user($this, $user)->trigger();
8374 return true;
8379 * Set the workflow state for multiple users
8381 * @return void
8383 protected function process_set_batch_marking_workflow_state() {
8384 global $CFG, $DB;
8386 // Include batch marking workflow form.
8387 require_once($CFG->dirroot . '/mod/assign/batchsetmarkingworkflowstateform.php');
8389 $formparams = array(
8390 'userscount' => 0, // This form is never re-displayed, so we don't need to
8391 'usershtml' => '', // initialise these parameters with real information.
8392 'markingworkflowstates' => $this->get_marking_workflow_states_for_current_user()
8395 $mform = new mod_assign_batch_set_marking_workflow_state_form(null, $formparams);
8397 if ($mform->is_cancelled()) {
8398 return true;
8401 if ($formdata = $mform->get_data()) {
8402 $useridlist = explode(',', $formdata->selectedusers);
8403 $state = $formdata->markingworkflowstate;
8405 foreach ($useridlist as $userid) {
8406 $flags = $this->get_user_flags($userid, true);
8408 $flags->workflowstate = $state;
8410 // Clear the mailed flag if notification is requested, the student hasn't been
8411 // notified previously, the student can access the assignment, and the state
8412 // is "Released".
8413 $modinfo = get_fast_modinfo($this->course, $userid);
8414 $cm = $modinfo->get_cm($this->get_course_module()->id);
8415 if ($formdata->sendstudentnotifications && $cm->uservisible &&
8416 $state == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8417 $flags->mailed = 0;
8420 $gradingdisabled = $this->grading_disabled($userid);
8422 // Will not apply update if user does not have permission to assign this workflow state.
8423 if (!$gradingdisabled && $this->update_user_flags($flags)) {
8424 // Update Gradebook.
8425 $grade = $this->get_user_grade($userid, true);
8426 // Fetch any feedback for this student.
8427 $gradebookplugin = $this->get_admin_config()->feedback_plugin_for_gradebook;
8428 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
8429 $plugin = $this->get_feedback_plugin_by_type($gradebookplugin);
8430 if ($plugin && $plugin->is_enabled() && $plugin->is_visible()) {
8431 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
8432 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
8433 $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
8435 $this->update_grade($grade);
8436 $assign = clone $this->get_instance();
8437 $assign->cmidnumber = $this->get_course_module()->idnumber;
8438 // Set assign gradebook feedback plugin status.
8439 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled();
8441 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8442 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $state)->trigger();
8449 * Set the marking allocation for multiple users
8451 * @return void
8453 protected function process_set_batch_marking_allocation() {
8454 global $CFG, $DB;
8456 // Include batch marking allocation form.
8457 require_once($CFG->dirroot . '/mod/assign/batchsetallocatedmarkerform.php');
8459 $formparams = array(
8460 'userscount' => 0, // This form is never re-displayed, so we don't need to
8461 'usershtml' => '' // initialise these parameters with real information.
8464 list($sort, $params) = users_order_by_sql('u');
8465 // Only enrolled users could be assigned as potential markers.
8466 $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
8467 $markerlist = array();
8468 foreach ($markers as $marker) {
8469 $markerlist[$marker->id] = fullname($marker);
8472 $formparams['markers'] = $markerlist;
8474 $mform = new mod_assign_batch_set_allocatedmarker_form(null, $formparams);
8476 if ($mform->is_cancelled()) {
8477 return true;
8480 if ($formdata = $mform->get_data()) {
8481 $useridlist = explode(',', $formdata->selectedusers);
8482 $marker = $DB->get_record('user', array('id' => $formdata->allocatedmarker), '*', MUST_EXIST);
8484 foreach ($useridlist as $userid) {
8485 $flags = $this->get_user_flags($userid, true);
8486 if ($flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW ||
8487 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW ||
8488 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE ||
8489 $flags->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8491 continue; // Allocated marker can only be changed in certain workflow states.
8494 $flags->allocatedmarker = $marker->id;
8496 if ($this->update_user_flags($flags)) {
8497 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8498 \mod_assign\event\marker_updated::create_from_marker($this, $user, $marker)->trigger();
8506 * Prevent student updates to this submission.
8507 * Uses url parameter userid.
8509 * @param int $userid
8510 * @return void
8512 protected function process_lock_submission($userid = 0) {
8514 require_sesskey();
8516 if (!$userid) {
8517 $userid = required_param('userid', PARAM_INT);
8520 return $this->lock_submission($userid);
8524 * Unlock the student submission.
8526 * @param int $userid
8527 * @return bool
8529 public function unlock_submission($userid) {
8530 global $USER, $DB;
8532 // Need grade permission.
8533 require_capability('mod/assign:grade', $this->context);
8535 // Give each submission plugin a chance to process the unlocking.
8536 $plugins = $this->get_submission_plugins();
8537 $submission = $this->get_user_submission($userid, false);
8539 $flags = $this->get_user_flags($userid, true);
8540 $flags->locked = 0;
8541 $this->update_user_flags($flags);
8543 foreach ($plugins as $plugin) {
8544 if ($plugin->is_enabled() && $plugin->is_visible()) {
8545 $plugin->unlock($submission, $flags);
8549 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8550 \mod_assign\event\submission_unlocked::create_from_user($this, $user)->trigger();
8551 return true;
8555 * Unlock the student submission.
8556 * Uses url parameter userid.
8558 * @param int $userid
8559 * @return bool
8561 protected function process_unlock_submission($userid = 0) {
8563 require_sesskey();
8565 if (!$userid) {
8566 $userid = required_param('userid', PARAM_INT);
8569 return $this->unlock_submission($userid);
8573 * Apply a grade from a grading form to a user (may be called multiple times for a group submission).
8575 * @param stdClass $formdata - the data from the form
8576 * @param int $userid - the user to apply the grade to
8577 * @param int $attemptnumber - The attempt number to apply the grade to.
8578 * @return void
8580 protected function apply_grade_to_user($formdata, $userid, $attemptnumber) {
8581 global $USER, $CFG, $DB;
8583 $grade = $this->get_user_grade($userid, true, $attemptnumber);
8584 $originalgrade = $grade->grade;
8585 $gradingdisabled = $this->grading_disabled($userid);
8586 $gradinginstance = $this->get_grading_instance($userid, $grade, $gradingdisabled);
8587 if (!$gradingdisabled) {
8588 if ($gradinginstance) {
8589 $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading,
8590 $grade->id);
8591 } else {
8592 // Handle the case when grade is set to No Grade.
8593 if (isset($formdata->grade)) {
8594 $grade->grade = grade_floatval(unformat_float($formdata->grade));
8597 if (isset($formdata->workflowstate) || isset($formdata->allocatedmarker)) {
8598 $flags = $this->get_user_flags($userid, true);
8599 $oldworkflowstate = $flags->workflowstate;
8600 $flags->workflowstate = isset($formdata->workflowstate) ? $formdata->workflowstate : $flags->workflowstate;
8601 $flags->allocatedmarker = isset($formdata->allocatedmarker) ? $formdata->allocatedmarker : $flags->allocatedmarker;
8602 if ($this->update_user_flags($flags) &&
8603 isset($formdata->workflowstate) &&
8604 $formdata->workflowstate !== $oldworkflowstate) {
8605 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
8606 \mod_assign\event\workflow_state_updated::create_from_user($this, $user, $formdata->workflowstate)->trigger();
8610 $grade->grader= $USER->id;
8612 $adminconfig = $this->get_admin_config();
8613 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
8615 $feedbackmodified = false;
8617 // Call save in plugins.
8618 foreach ($this->feedbackplugins as $plugin) {
8619 if ($plugin->is_enabled() && $plugin->is_visible()) {
8620 $gradingmodified = $plugin->is_feedback_modified($grade, $formdata);
8621 if ($gradingmodified) {
8622 if (!$plugin->save($grade, $formdata)) {
8623 $result = false;
8624 throw new \moodle_exception($plugin->get_error());
8626 // If $feedbackmodified is true, keep it true.
8627 $feedbackmodified = $feedbackmodified || $gradingmodified;
8629 if (('assignfeedback_' . $plugin->get_type()) == $gradebookplugin) {
8630 // This is the feedback plugin chose to push comments to the gradebook.
8631 $grade->feedbacktext = $plugin->text_for_gradebook($grade);
8632 $grade->feedbackformat = $plugin->format_for_gradebook($grade);
8633 $grade->feedbackfiles = $plugin->files_for_gradebook($grade);
8638 // We do not want to update the timemodified if no grade was added.
8639 if (!empty($formdata->addattempt) ||
8640 ($originalgrade !== null && $originalgrade != -1) ||
8641 ($grade->grade !== null && $grade->grade != -1) ||
8642 $feedbackmodified) {
8643 $this->update_grade($grade, !empty($formdata->addattempt));
8646 // We never send notifications if we have marking workflow and the grade is not released.
8647 if ($this->get_instance()->markingworkflow &&
8648 isset($formdata->workflowstate) &&
8649 $formdata->workflowstate != ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
8650 $formdata->sendstudentnotifications = false;
8653 // Note the default if not provided for this option is true (e.g. webservices).
8654 // This is for backwards compatibility.
8655 if (!isset($formdata->sendstudentnotifications) || $formdata->sendstudentnotifications) {
8656 $this->notify_grade_modified($grade, true);
8662 * Save outcomes submitted from grading form.
8664 * @param int $userid
8665 * @param stdClass $formdata
8666 * @param int $sourceuserid The user ID under which the outcome data is accessible. This is relevant
8667 * for an outcome set to a user but applied to an entire group.
8669 protected function process_outcomes($userid, $formdata, $sourceuserid = null) {
8670 global $CFG, $USER;
8672 if (empty($CFG->enableoutcomes)) {
8673 return;
8675 if ($this->grading_disabled($userid)) {
8676 return;
8679 require_once($CFG->libdir.'/gradelib.php');
8681 $data = array();
8682 $gradinginfo = grade_get_grades($this->get_course()->id,
8683 'mod',
8684 'assign',
8685 $this->get_instance()->id,
8686 $userid);
8688 if (!empty($gradinginfo->outcomes)) {
8689 foreach ($gradinginfo->outcomes as $index => $oldoutcome) {
8690 $name = 'outcome_'.$index;
8691 $sourceuserid = $sourceuserid !== null ? $sourceuserid : $userid;
8692 if (isset($formdata->{$name}[$sourceuserid]) &&
8693 $oldoutcome->grades[$userid]->grade != $formdata->{$name}[$sourceuserid]) {
8694 $data[$index] = $formdata->{$name}[$sourceuserid];
8698 if (count($data) > 0) {
8699 grade_update_outcomes('mod/assign',
8700 $this->course->id,
8701 'mod',
8702 'assign',
8703 $this->get_instance()->id,
8704 $userid,
8705 $data);
8710 * If the requirements are met - reopen the submission for another attempt.
8711 * Only call this function when grading the latest attempt.
8713 * @param int $userid The userid.
8714 * @param stdClass $submission The submission (may be a group submission).
8715 * @param bool $addattempt - True if the "allow another attempt" checkbox was checked.
8716 * @return bool - true if another attempt was added.
8718 protected function reopen_submission_if_required($userid, $submission, $addattempt) {
8719 $instance = $this->get_instance();
8720 $maxattemptsreached = !empty($submission) &&
8721 $submission->attemptnumber >= ($instance->maxattempts - 1) &&
8722 $instance->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS;
8723 $shouldreopen = false;
8724 if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS) {
8725 // Check the gradetopass from the gradebook.
8726 $gradeitem = $this->get_grade_item();
8727 if ($gradeitem) {
8728 $gradegrade = grade_grade::fetch(array('userid' => $userid, 'itemid' => $gradeitem->id));
8730 // Do not reopen if is_passed returns null, e.g. if there is no pass criterion set.
8731 if ($gradegrade && ($gradegrade->is_passed() === false)) {
8732 $shouldreopen = true;
8736 if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL &&
8737 !empty($addattempt)) {
8738 $shouldreopen = true;
8740 if ($shouldreopen && !$maxattemptsreached) {
8741 $this->add_attempt($userid);
8742 return true;
8744 return false;
8748 * Save grade update.
8750 * @param int $userid
8751 * @param stdClass $data
8752 * @return bool - was the grade saved
8754 public function save_grade($userid, $data) {
8756 // Need grade permission.
8757 require_capability('mod/assign:grade', $this->context);
8759 $instance = $this->get_instance();
8760 $submission = null;
8761 if ($instance->teamsubmission) {
8762 // We need to know what the most recent group submission is.
8763 // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
8764 // and when deciding if we need to update the gradebook with an edited grade.
8765 $mostrecentsubmission = $this->get_group_submission($userid, 0, false, -1);
8766 $this->set_most_recent_team_submission($mostrecentsubmission);
8767 // Get the submission that we are saving grades for. The data attempt number determines which submission attempt.
8768 $submission = $this->get_group_submission($userid, 0, false, $data->attemptnumber);
8769 } else {
8770 $submission = $this->get_user_submission($userid, false, $data->attemptnumber);
8772 if ($instance->teamsubmission && !empty($data->applytoall)) {
8773 $groupid = 0;
8774 if ($this->get_submission_group($userid)) {
8775 $group = $this->get_submission_group($userid);
8776 if ($group) {
8777 $groupid = $group->id;
8780 $members = $this->get_submission_group_members($groupid, true, $this->show_only_active_users());
8781 foreach ($members as $member) {
8782 // We only want to update the grade for this group submission attempt. The data attempt number could be
8783 // -1 which may end up in additional attempts being created for each group member instead of just one
8784 // additional attempt for the group.
8785 $this->apply_grade_to_user($data, $member->id, $submission->attemptnumber);
8786 $this->process_outcomes($member->id, $data, $userid);
8788 } else {
8789 $this->apply_grade_to_user($data, $userid, $data->attemptnumber);
8791 $this->process_outcomes($userid, $data);
8794 return true;
8798 * Save grade.
8800 * @param moodleform $mform
8801 * @return bool - was the grade saved
8803 protected function process_save_grade(&$mform) {
8804 global $CFG, $SESSION;
8805 // Include grade form.
8806 require_once($CFG->dirroot . '/mod/assign/gradeform.php');
8808 require_sesskey();
8810 $instance = $this->get_instance();
8811 $rownum = required_param('rownum', PARAM_INT);
8812 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
8813 $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
8814 $userid = optional_param('userid', 0, PARAM_INT);
8815 if (!$userid) {
8816 if (empty($SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)])) {
8817 // If the userid list is not stored we must not save, as it is possible that the user in a
8818 // given row position may not be the same now as when the grading page was generated.
8819 $url = new moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
8820 throw new moodle_exception('useridlistnotcached', 'mod_assign', $url);
8822 $useridlist = $SESSION->mod_assign_useridlist[$this->get_useridlist_key($useridlistid)];
8823 } else {
8824 $useridlist = array($userid);
8825 $rownum = 0;
8828 $last = false;
8829 $userid = $useridlist[$rownum];
8830 if ($rownum == count($useridlist) - 1) {
8831 $last = true;
8834 $data = new stdClass();
8836 $gradeformparams = array('rownum' => $rownum,
8837 'useridlistid' => $useridlistid,
8838 'last' => $last,
8839 'attemptnumber' => $attemptnumber,
8840 'userid' => $userid);
8841 $mform = new mod_assign_grade_form(null,
8842 array($this, $data, $gradeformparams),
8843 'post',
8845 array('class'=>'gradeform'));
8847 if ($formdata = $mform->get_data()) {
8848 return $this->save_grade($userid, $formdata);
8849 } else {
8850 return false;
8855 * This function is a static wrapper around can_upgrade.
8857 * @param string $type The plugin type
8858 * @param int $version The plugin version
8859 * @return bool
8861 public static function can_upgrade_assignment($type, $version) {
8862 $assignment = new assign(null, null, null);
8863 return $assignment->can_upgrade($type, $version);
8867 * This function returns true if it can upgrade an assignment from the 2.2 module.
8869 * @param string $type The plugin type
8870 * @param int $version The plugin version
8871 * @return bool
8873 public function can_upgrade($type, $version) {
8874 if ($type == 'offline' && $version >= 2011112900) {
8875 return true;
8877 foreach ($this->submissionplugins as $plugin) {
8878 if ($plugin->can_upgrade($type, $version)) {
8879 return true;
8882 foreach ($this->feedbackplugins as $plugin) {
8883 if ($plugin->can_upgrade($type, $version)) {
8884 return true;
8887 return false;
8891 * Copy all the files from the old assignment files area to the new one.
8892 * This is used by the plugin upgrade code.
8894 * @param int $oldcontextid The old assignment context id
8895 * @param int $oldcomponent The old assignment component ('assignment')
8896 * @param int $oldfilearea The old assignment filearea ('submissions')
8897 * @param int $olditemid The old submissionid (can be null e.g. intro)
8898 * @param int $newcontextid The new assignment context id
8899 * @param int $newcomponent The new assignment component ('assignment')
8900 * @param int $newfilearea The new assignment filearea ('submissions')
8901 * @param int $newitemid The new submissionid (can be null e.g. intro)
8902 * @return int The number of files copied
8904 public function copy_area_files_for_upgrade($oldcontextid,
8905 $oldcomponent,
8906 $oldfilearea,
8907 $olditemid,
8908 $newcontextid,
8909 $newcomponent,
8910 $newfilearea,
8911 $newitemid) {
8912 // Note, this code is based on some code in filestorage - but that code
8913 // deleted the old files (which we don't want).
8914 $count = 0;
8916 $fs = get_file_storage();
8918 $oldfiles = $fs->get_area_files($oldcontextid,
8919 $oldcomponent,
8920 $oldfilearea,
8921 $olditemid,
8922 'id',
8923 false);
8924 foreach ($oldfiles as $oldfile) {
8925 $filerecord = new stdClass();
8926 $filerecord->contextid = $newcontextid;
8927 $filerecord->component = $newcomponent;
8928 $filerecord->filearea = $newfilearea;
8929 $filerecord->itemid = $newitemid;
8930 $fs->create_file_from_storedfile($filerecord, $oldfile);
8931 $count += 1;
8934 return $count;
8938 * Add a new attempt for each user in the list - but reopen each group assignment
8939 * at most 1 time.
8941 * @param array $useridlist Array of userids to reopen.
8942 * @return bool
8944 protected function process_add_attempt_group($useridlist) {
8945 $groupsprocessed = array();
8946 $result = true;
8948 foreach ($useridlist as $userid) {
8949 $groupid = 0;
8950 $group = $this->get_submission_group($userid);
8951 if ($group) {
8952 $groupid = $group->id;
8955 if (empty($groupsprocessed[$groupid])) {
8956 // We need to know what the most recent group submission is.
8957 // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
8958 // and when deciding if we need to update the gradebook with an edited grade.
8959 $currentsubmission = $this->get_group_submission($userid, 0, false, -1);
8960 $this->set_most_recent_team_submission($currentsubmission);
8961 $result = $this->process_add_attempt($userid) && $result;
8962 $groupsprocessed[$groupid] = true;
8965 return $result;
8969 * Check for a sess key and then call add_attempt.
8971 * @param int $userid int The user to add the attempt for
8972 * @return bool - true if successful.
8974 protected function process_add_attempt($userid) {
8975 require_sesskey();
8977 return $this->add_attempt($userid);
8981 * Add a new attempt for a user.
8983 * @param int $userid int The user to add the attempt for
8984 * @return bool - true if successful.
8986 protected function add_attempt($userid) {
8987 require_capability('mod/assign:grade', $this->context);
8989 if ($this->get_instance()->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_NONE) {
8990 return false;
8993 if ($this->get_instance()->teamsubmission) {
8994 $oldsubmission = $this->get_group_submission($userid, 0, false);
8995 } else {
8996 $oldsubmission = $this->get_user_submission($userid, false);
8999 if (!$oldsubmission) {
9000 return false;
9003 // No more than max attempts allowed.
9004 if ($this->get_instance()->maxattempts != ASSIGN_UNLIMITED_ATTEMPTS &&
9005 $oldsubmission->attemptnumber >= ($this->get_instance()->maxattempts - 1)) {
9006 return false;
9009 // Create the new submission record for the group/user.
9010 if ($this->get_instance()->teamsubmission) {
9011 if (isset($this->mostrecentteamsubmission)) {
9012 // Team submissions can end up in this function for each user (via save_grade). We don't want to create
9013 // more than one attempt for the whole team.
9014 if ($this->mostrecentteamsubmission->attemptnumber == $oldsubmission->attemptnumber) {
9015 $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
9016 } else {
9017 $newsubmission = $this->get_group_submission($userid, 0, false, $oldsubmission->attemptnumber);
9019 } else {
9020 debugging('Please use set_most_recent_team_submission() before calling add_attempt', DEBUG_DEVELOPER);
9021 $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
9023 } else {
9024 $newsubmission = $this->get_user_submission($userid, true, $oldsubmission->attemptnumber + 1);
9027 // Set the status of the new attempt to reopened.
9028 $newsubmission->status = ASSIGN_SUBMISSION_STATUS_REOPENED;
9030 // Give each submission plugin a chance to process the add_attempt.
9031 $plugins = $this->get_submission_plugins();
9032 foreach ($plugins as $plugin) {
9033 if ($plugin->is_enabled() && $plugin->is_visible()) {
9034 $plugin->add_attempt($oldsubmission, $newsubmission);
9038 $this->update_submission($newsubmission, $userid, false, $this->get_instance()->teamsubmission);
9039 $flags = $this->get_user_flags($userid, false);
9040 if (isset($flags->locked) && $flags->locked) { // May not exist.
9041 $this->process_unlock_submission($userid);
9043 return true;
9047 * Get an upto date list of user grades and feedback for the gradebook.
9049 * @param int $userid int or 0 for all users
9050 * @return array of grade data formated for the gradebook api
9051 * The data required by the gradebook api is userid,
9052 * rawgrade,
9053 * feedback,
9054 * feedbackformat,
9055 * usermodified,
9056 * dategraded,
9057 * datesubmitted
9059 public function get_user_grades_for_gradebook($userid) {
9060 global $DB, $CFG;
9061 $grades = array();
9062 $assignmentid = $this->get_instance()->id;
9064 $adminconfig = $this->get_admin_config();
9065 $gradebookpluginname = $adminconfig->feedback_plugin_for_gradebook;
9066 $gradebookplugin = null;
9068 // Find the gradebook plugin.
9069 foreach ($this->feedbackplugins as $plugin) {
9070 if ($plugin->is_enabled() && $plugin->is_visible()) {
9071 if (('assignfeedback_' . $plugin->get_type()) == $gradebookpluginname) {
9072 $gradebookplugin = $plugin;
9076 if ($userid) {
9077 $where = ' WHERE u.id = :userid ';
9078 } else {
9079 $where = ' WHERE u.id != :userid ';
9082 // When the gradebook asks us for grades - only return the last attempt for each user.
9083 $params = array('assignid1'=>$assignmentid,
9084 'assignid2'=>$assignmentid,
9085 'userid'=>$userid);
9086 $graderesults = $DB->get_recordset_sql('SELECT
9087 u.id as userid,
9088 s.timemodified as datesubmitted,
9089 g.grade as rawgrade,
9090 g.timemodified as dategraded,
9091 g.grader as usermodified
9092 FROM {user} u
9093 LEFT JOIN {assign_submission} s
9094 ON u.id = s.userid and s.assignment = :assignid1 AND
9095 s.latest = 1
9096 JOIN {assign_grades} g
9097 ON u.id = g.userid and g.assignment = :assignid2 AND
9098 g.attemptnumber = s.attemptnumber' .
9099 $where, $params);
9101 foreach ($graderesults as $result) {
9102 $gradingstatus = $this->get_grading_status($result->userid);
9103 if (!$this->get_instance()->markingworkflow || $gradingstatus == ASSIGN_MARKING_WORKFLOW_STATE_RELEASED) {
9104 $gradebookgrade = clone $result;
9105 // Now get the feedback.
9106 if ($gradebookplugin) {
9107 $grade = $this->get_user_grade($result->userid, false);
9108 if ($grade) {
9109 $feedbacktext = $gradebookplugin->text_for_gradebook($grade);
9110 if (!empty($feedbacktext)) {
9111 $gradebookgrade->feedback = $feedbacktext;
9113 $gradebookgrade->feedbackformat = $gradebookplugin->format_for_gradebook($grade);
9114 $gradebookgrade->feedbackfiles = $gradebookplugin->files_for_gradebook($grade);
9117 $grades[$gradebookgrade->userid] = $gradebookgrade;
9121 $graderesults->close();
9122 return $grades;
9126 * Call the static version of this function
9128 * @param int $userid The userid to lookup
9129 * @return int The unique id
9131 public function get_uniqueid_for_user($userid) {
9132 return self::get_uniqueid_for_user_static($this->get_instance()->id, $userid);
9136 * Foreach participant in the course - assign them a random id.
9138 * @param int $assignid The assignid to lookup
9140 public static function allocate_unique_ids($assignid) {
9141 global $DB;
9143 $cm = get_coursemodule_from_instance('assign', $assignid, 0, false, MUST_EXIST);
9144 $context = context_module::instance($cm->id);
9146 $currentgroup = groups_get_activity_group($cm, true);
9147 $users = get_enrolled_users($context, "mod/assign:submit", $currentgroup, 'u.id');
9149 // Shuffle the users.
9150 shuffle($users);
9152 foreach ($users as $user) {
9153 $record = $DB->get_record('assign_user_mapping',
9154 array('assignment'=>$assignid, 'userid'=>$user->id),
9155 'id');
9156 if (!$record) {
9157 $record = new stdClass();
9158 $record->assignment = $assignid;
9159 $record->userid = $user->id;
9160 $DB->insert_record('assign_user_mapping', $record);
9166 * Lookup this user id and return the unique id for this assignment.
9168 * @param int $assignid The assignment id
9169 * @param int $userid The userid to lookup
9170 * @return int The unique id
9172 public static function get_uniqueid_for_user_static($assignid, $userid) {
9173 global $DB;
9175 // Search for a record.
9176 $params = array('assignment'=>$assignid, 'userid'=>$userid);
9177 if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
9178 return $record->id;
9181 // Be a little smart about this - there is no record for the current user.
9182 // We should ensure any unallocated ids for the current participant
9183 // list are distrubited randomly.
9184 self::allocate_unique_ids($assignid);
9186 // Retry the search for a record.
9187 if ($record = $DB->get_record('assign_user_mapping', $params, 'id')) {
9188 return $record->id;
9191 // The requested user must not be a participant. Add a record anyway.
9192 $record = new stdClass();
9193 $record->assignment = $assignid;
9194 $record->userid = $userid;
9196 return $DB->insert_record('assign_user_mapping', $record);
9200 * Call the static version of this function.
9202 * @param int $uniqueid The uniqueid to lookup
9203 * @return int The user id or false if they don't exist
9205 public function get_user_id_for_uniqueid($uniqueid) {
9206 return self::get_user_id_for_uniqueid_static($this->get_instance()->id, $uniqueid);
9210 * Lookup this unique id and return the user id for this assignment.
9212 * @param int $assignid The id of the assignment this user mapping is in
9213 * @param int $uniqueid The uniqueid to lookup
9214 * @return int The user id or false if they don't exist
9216 public static function get_user_id_for_uniqueid_static($assignid, $uniqueid) {
9217 global $DB;
9219 // Search for a record.
9220 if ($record = $DB->get_record('assign_user_mapping',
9221 array('assignment'=>$assignid, 'id'=>$uniqueid),
9222 'userid',
9223 IGNORE_MISSING)) {
9224 return $record->userid;
9227 return false;
9231 * Get the list of marking_workflow states the current user has permission to transition a grade to.
9233 * @return array of state => description
9235 public function get_marking_workflow_states_for_current_user() {
9236 if (!empty($this->markingworkflowstates)) {
9237 return $this->markingworkflowstates;
9239 $states = array();
9240 if (has_capability('mod/assign:grade', $this->context)) {
9241 $states[ASSIGN_MARKING_WORKFLOW_STATE_INMARKING] = get_string('markingworkflowstateinmarking', 'assign');
9242 $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW] = get_string('markingworkflowstatereadyforreview', 'assign');
9244 if (has_any_capability(array('mod/assign:reviewgrades',
9245 'mod/assign:managegrades'), $this->context)) {
9246 $states[ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW] = get_string('markingworkflowstateinreview', 'assign');
9247 $states[ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE] = get_string('markingworkflowstatereadyforrelease', 'assign');
9249 if (has_any_capability(array('mod/assign:releasegrades',
9250 'mod/assign:managegrades'), $this->context)) {
9251 $states[ASSIGN_MARKING_WORKFLOW_STATE_RELEASED] = get_string('markingworkflowstatereleased', 'assign');
9253 $this->markingworkflowstates = $states;
9254 return $this->markingworkflowstates;
9258 * Check is only active users in course should be shown.
9260 * @return bool true if only active users should be shown.
9262 public function show_only_active_users() {
9263 global $CFG;
9265 if (is_null($this->showonlyactiveenrol)) {
9266 $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
9267 $this->showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol);
9269 if (!is_null($this->context)) {
9270 $this->showonlyactiveenrol = $this->showonlyactiveenrol ||
9271 !has_capability('moodle/course:viewsuspendedusers', $this->context);
9274 return $this->showonlyactiveenrol;
9278 * Return true is user is active user in course else false
9280 * @param int $userid
9281 * @return bool true is user is active in course.
9283 public function is_active_user($userid) {
9284 return !in_array($userid, get_suspended_userids($this->context, true));
9288 * Returns true if gradebook feedback plugin is enabled
9290 * @return bool true if gradebook feedback plugin is enabled and visible else false.
9292 public function is_gradebook_feedback_enabled() {
9293 // Get default grade book feedback plugin.
9294 $adminconfig = $this->get_admin_config();
9295 $gradebookplugin = $adminconfig->feedback_plugin_for_gradebook;
9296 $gradebookplugin = str_replace('assignfeedback_', '', $gradebookplugin);
9298 // Check if default gradebook feedback is visible and enabled.
9299 $gradebookfeedbackplugin = $this->get_feedback_plugin_by_type($gradebookplugin);
9301 if (empty($gradebookfeedbackplugin)) {
9302 return false;
9305 if ($gradebookfeedbackplugin->is_visible() && $gradebookfeedbackplugin->is_enabled()) {
9306 return true;
9309 // Gradebook feedback plugin is either not visible/enabled.
9310 return false;
9314 * Returns the grading status.
9316 * @param int $userid the user id
9317 * @return string returns the grading status
9319 public function get_grading_status($userid) {
9320 if ($this->get_instance()->markingworkflow) {
9321 $flags = $this->get_user_flags($userid, false);
9322 if (!empty($flags->workflowstate)) {
9323 return $flags->workflowstate;
9325 return ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED;
9326 } else {
9327 $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
9328 $grade = $this->get_user_grade($userid, false, $attemptnumber);
9330 if (!empty($grade) && $grade->grade !== null && $grade->grade >= 0) {
9331 return ASSIGN_GRADING_STATUS_GRADED;
9332 } else {
9333 return ASSIGN_GRADING_STATUS_NOT_GRADED;
9339 * The id used to uniquily identify the cache for this instance of the assign object.
9341 * @return string
9343 public function get_useridlist_key_id() {
9344 return $this->useridlistid;
9348 * Generates the key that should be used for an entry in the useridlist cache.
9350 * @param string $id Generate a key for this instance (optional)
9351 * @return string The key for the id, or new entry if no $id is passed.
9353 public function get_useridlist_key($id = null) {
9354 global $SESSION;
9356 // Ensure the user id list cache is initialised.
9357 if (!isset($SESSION->mod_assign_useridlist)) {
9358 $SESSION->mod_assign_useridlist = [];
9361 if ($id === null) {
9362 $id = $this->get_useridlist_key_id();
9364 return $this->get_course_module()->id . '_' . $id;
9368 * Updates and creates the completion records in mdl_course_modules_completion.
9370 * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity
9371 * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit
9372 * @param obj $submission the submission
9373 * @param int $userid the user id
9374 * @param int $complete
9375 * @param obj $completion
9377 * @return null
9379 protected function update_activity_completion_records($teamsubmission,
9380 $requireallteammemberssubmit,
9381 $submission,
9382 $userid,
9383 $complete,
9384 $completion) {
9386 if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) ||
9387 ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit &&
9388 $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
9390 $members = groups_get_members($submission->groupid);
9392 foreach ($members as $member) {
9393 $completion->update_state($this->get_course_module(), $complete, $member->id);
9395 } else {
9396 $completion->update_state($this->get_course_module(), $complete, $userid);
9399 return;
9403 * Update the module completion status (set it viewed) and trigger module viewed event.
9405 * @since Moodle 3.2
9407 public function set_module_viewed() {
9408 $completion = new completion_info($this->get_course());
9409 $completion->set_module_viewed($this->get_course_module());
9411 // Trigger the course module viewed event.
9412 $assigninstance = $this->get_instance();
9413 $params = [
9414 'objectid' => $assigninstance->id,
9415 'context' => $this->get_context()
9417 if ($this->is_blind_marking()) {
9418 $params['anonymous'] = 1;
9421 $event = \mod_assign\event\course_module_viewed::create($params);
9423 $event->add_record_snapshot('assign', $assigninstance);
9424 $event->trigger();
9428 * Checks for any grade notices, and adds notifications. Will display on assignment main page and grading table.
9430 * @return void The notifications API will render the notifications at the appropriate part of the page.
9432 protected function add_grade_notices() {
9433 if (has_capability('mod/assign:grade', $this->get_context()) && get_config('assign', 'has_rescaled_null_grades_' . $this->get_instance()->id)) {
9434 $link = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades'));
9435 \core\notification::warning(get_string('fixrescalednullgrades', 'mod_assign', ['link' => $link->out()]));
9440 * View fix rescaled null grades.
9442 * @return bool True if null all grades are now fixed.
9444 protected function fix_null_grades() {
9445 global $DB;
9446 $result = $DB->set_field_select(
9447 'assign_grades',
9448 'grade',
9449 ASSIGN_GRADE_NOT_SET,
9450 'grade <> ? AND grade < 0',
9451 [ASSIGN_GRADE_NOT_SET]
9453 $assign = clone $this->get_instance();
9454 $assign->cmidnumber = $this->get_course_module()->idnumber;
9455 assign_update_grades($assign);
9456 return $result;
9460 * View fix rescaled null grades.
9462 * @return void The notifications API will render the notifications at the appropriate part of the page.
9464 protected function view_fix_rescaled_null_grades() {
9465 global $OUTPUT;
9467 $o = '';
9469 require_capability('mod/assign:grade', $this->get_context());
9471 $instance = $this->get_instance();
9473 $o .= $this->get_renderer()->render(
9474 new assign_header(
9475 $instance,
9476 $this->get_context(),
9477 $this->show_intro(),
9478 $this->get_course_module()->id
9482 $confirm = optional_param('confirm', 0, PARAM_BOOL);
9484 if ($confirm) {
9485 confirm_sesskey();
9487 // Fix the grades.
9488 $this->fix_null_grades();
9489 unset_config('has_rescaled_null_grades_' . $instance->id, 'assign');
9491 // Display the notice.
9492 $o .= $this->get_renderer()->notification(get_string('fixrescalednullgradesdone', 'assign'), 'notifysuccess');
9493 $url = new moodle_url(
9494 '/mod/assign/view.php',
9495 array(
9496 'id' => $this->get_course_module()->id,
9497 'action' => 'grading'
9500 $o .= $this->get_renderer()->continue_button($url);
9501 } else {
9502 // Ask for confirmation.
9503 $continue = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id, 'action' => 'fixrescalednullgrades', 'confirm' => true, 'sesskey' => sesskey()));
9504 $cancel = new \moodle_url('/mod/assign/view.php', array('id' => $this->get_course_module()->id));
9505 $o .= $OUTPUT->confirm(get_string('fixrescalednullgradesconfirm', 'mod_assign'), $continue, $cancel);
9508 $o .= $this->view_footer();
9510 return $o;
9514 * Set the most recent submission for the team.
9515 * The most recent team submission is used to determine if another attempt should be created when allowing another
9516 * attempt on a group assignment, and whether the gradebook should be updated.
9518 * @since Moodle 3.4
9519 * @param stdClass $submission The most recent submission of the group.
9521 public function set_most_recent_team_submission($submission) {
9522 $this->mostrecentteamsubmission = $submission;
9526 * Return array of valid grading allocation filters for the grading interface.
9528 * @param boolean $export Export the list of filters for a template.
9529 * @return array
9531 public function get_marking_allocation_filters($export = false) {
9532 $markingallocation = $this->get_instance()->markingworkflow &&
9533 $this->get_instance()->markingallocation &&
9534 has_capability('mod/assign:manageallocations', $this->context);
9535 // Get markers to use in drop lists.
9536 $markingallocationoptions = array();
9537 if ($markingallocation) {
9538 list($sort, $params) = users_order_by_sql('u');
9539 // Only enrolled users could be assigned as potential markers.
9540 $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
9541 $markingallocationoptions[''] = get_string('filternone', 'assign');
9542 $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
9543 $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
9544 foreach ($markers as $marker) {
9545 $markingallocationoptions[$marker->id] = fullname($marker, $viewfullnames);
9548 if ($export) {
9549 $allocationfilter = get_user_preferences('assign_markerfilter', '');
9550 $result = [];
9551 foreach ($markingallocationoptions as $option => $label) {
9552 array_push($result, [
9553 'key' => $option,
9554 'name' => $label,
9555 'active' => ($allocationfilter == $option),
9558 return $result;
9560 return $markingworkflowoptions;
9564 * Return array of valid grading workflow filters for the grading interface.
9566 * @param boolean $export Export the list of filters for a template.
9567 * @return array
9569 public function get_marking_workflow_filters($export = false) {
9570 $markingworkflow = $this->get_instance()->markingworkflow;
9571 // Get marking states to show in form.
9572 $markingworkflowoptions = array();
9573 if ($markingworkflow) {
9574 $notmarked = get_string('markingworkflowstatenotmarked', 'assign');
9575 $markingworkflowoptions[''] = get_string('filternone', 'assign');
9576 $markingworkflowoptions[ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED] = $notmarked;
9577 $markingworkflowoptions = array_merge($markingworkflowoptions, $this->get_marking_workflow_states_for_current_user());
9579 if ($export) {
9580 $workflowfilter = get_user_preferences('assign_workflowfilter', '');
9581 $result = [];
9582 foreach ($markingworkflowoptions as $option => $label) {
9583 array_push($result, [
9584 'key' => $option,
9585 'name' => $label,
9586 'active' => ($workflowfilter == $option),
9589 return $result;
9591 return $markingworkflowoptions;
9595 * Return array of valid search filters for the grading interface.
9597 * @return array
9599 public function get_filters() {
9600 $filterkeys = [
9601 ASSIGN_FILTER_NOT_SUBMITTED,
9602 ASSIGN_FILTER_DRAFT,
9603 ASSIGN_FILTER_SUBMITTED,
9604 ASSIGN_FILTER_REQUIRE_GRADING,
9605 ASSIGN_FILTER_GRANTED_EXTENSION
9608 $current = get_user_preferences('assign_filter', '');
9610 $filters = [];
9611 // First is always "no filter" option.
9612 array_push($filters, [
9613 'key' => 'none',
9614 'name' => get_string('filternone', 'assign'),
9615 'active' => ($current == '')
9618 foreach ($filterkeys as $key) {
9619 array_push($filters, [
9620 'key' => $key,
9621 'name' => get_string('filter' . $key, 'assign'),
9622 'active' => ($current == $key)
9625 return $filters;
9629 * Get the correct submission statement depending on single submisison, team submission or team submission
9630 * where all team memebers must submit.
9632 * @param array $adminconfig
9633 * @param assign $instance
9634 * @param context $context
9636 * @return string
9638 protected function get_submissionstatement($adminconfig, $instance, $context) {
9639 $submissionstatement = '';
9641 if (!($context instanceof context)) {
9642 return $submissionstatement;
9645 // Single submission.
9646 if (!$instance->teamsubmission) {
9647 // Single submission statement is not empty.
9648 if (!empty($adminconfig->submissionstatement)) {
9649 // Format the submission statement before its sent. We turn off para because this is going within
9650 // a form element.
9651 $options = array(
9652 'context' => $context,
9653 'para' => false
9655 $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
9657 } else { // Team submission.
9658 // One user can submit for the whole team.
9659 if (!empty($adminconfig->submissionstatementteamsubmission) && !$instance->requireallteammemberssubmit) {
9660 // Format the submission statement before its sent. We turn off para because this is going within
9661 // a form element.
9662 $options = array(
9663 'context' => $context,
9664 'para' => false
9666 $submissionstatement = format_text($adminconfig->submissionstatementteamsubmission,
9667 FORMAT_MOODLE, $options);
9668 } else if (!empty($adminconfig->submissionstatementteamsubmissionallsubmit) &&
9669 $instance->requireallteammemberssubmit) {
9670 // All team members must submit.
9671 // Format the submission statement before its sent. We turn off para because this is going within
9672 // a form element.
9673 $options = array(
9674 'context' => $context,
9675 'para' => false
9677 $submissionstatement = format_text($adminconfig->submissionstatementteamsubmissionallsubmit,
9678 FORMAT_MOODLE, $options);
9682 return $submissionstatement;
9686 * Check if time limit for assignment enabled and set up.
9688 * @param int|null $userid User ID. If null, use global user.
9689 * @return bool
9691 public function is_time_limit_enabled(?int $userid = null): bool {
9692 $instance = $this->get_instance($userid);
9693 return get_config('assign', 'enabletimelimit') && !empty($instance->timelimit);
9697 * Check if an assignment submission is already started and not yet submitted.
9699 * @param int|null $userid User ID. If null, use global user.
9700 * @param int $groupid Group ID. If 0, use user id to determine group.
9701 * @param int $attemptnumber Attempt number. If -1, check latest submission.
9702 * @return bool
9704 public function is_attempt_in_progress(?int $userid = null, int $groupid = 0, int $attemptnumber = -1): bool {
9705 if ($this->get_instance($userid)->teamsubmission) {
9706 $submission = $this->get_group_submission($userid, $groupid, false, $attemptnumber);
9707 } else {
9708 $submission = $this->get_user_submission($userid, false, $attemptnumber);
9711 // If time limit is enabled, we only assume it is in progress if there is a start time for submission.
9712 $timedattemptstarted = true;
9713 if ($this->is_time_limit_enabled($userid)) {
9714 $timedattemptstarted = !empty($submission) && !empty($submission->timestarted);
9717 return !empty($submission) && $submission->status !== ASSIGN_SUBMISSION_STATUS_SUBMITTED && $timedattemptstarted;
9722 * Portfolio caller class for mod_assign.
9724 * @package mod_assign
9725 * @copyright 2012 NetSpot {@link http://www.netspot.com.au}
9726 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
9728 class assign_portfolio_caller extends portfolio_module_caller_base {
9730 /** @var int callback arg - the id of submission we export */
9731 protected $sid;
9733 /** @var string component of the submission files we export*/
9734 protected $component;
9736 /** @var string callback arg - the area of submission files we export */
9737 protected $area;
9739 /** @var int callback arg - the id of file we export */
9740 protected $fileid;
9742 /** @var int callback arg - the cmid of the assignment we export */
9743 protected $cmid;
9745 /** @var string callback arg - the plugintype of the editor we export */
9746 protected $plugin;
9748 /** @var string callback arg - the name of the editor field we export */
9749 protected $editor;
9752 * Callback arg for a single file export.
9754 public static function expected_callbackargs() {
9755 return array(
9756 'cmid' => true,
9757 'sid' => false,
9758 'area' => false,
9759 'component' => false,
9760 'fileid' => false,
9761 'plugin' => false,
9762 'editor' => false,
9767 * The constructor.
9769 * @param array $callbackargs
9771 public function __construct($callbackargs) {
9772 parent::__construct($callbackargs);
9773 $this->cm = get_coursemodule_from_id('assign', $this->cmid, 0, false, MUST_EXIST);
9777 * Load data needed for the portfolio export.
9779 * If the assignment type implements portfolio_load_data(), the processing is delegated
9780 * to it. Otherwise, the caller must provide either fileid (to export single file) or
9781 * submissionid and filearea (to export all data attached to the given submission file area)
9782 * via callback arguments.
9784 * @throws portfolio_caller_exception
9786 public function load_data() {
9787 global $DB;
9789 $context = context_module::instance($this->cmid);
9791 if (empty($this->fileid)) {
9792 if (empty($this->sid) || empty($this->area)) {
9793 throw new portfolio_caller_exception('invalidfileandsubmissionid', 'mod_assign');
9796 $submission = $DB->get_record('assign_submission', array('id' => $this->sid));
9797 } else {
9798 $submissionid = $DB->get_field('files', 'itemid', array('id' => $this->fileid, 'contextid' => $context->id));
9799 if ($submissionid) {
9800 $submission = $DB->get_record('assign_submission', array('id' => $submissionid));
9804 if (empty($submission)) {
9805 throw new portfolio_caller_exception('filenotfound');
9806 } else if ($submission->userid == 0) {
9807 // This must be a group submission.
9808 if (!groups_is_member($submission->groupid, $this->user->id)) {
9809 throw new portfolio_caller_exception('filenotfound');
9811 } else if ($this->user->id != $submission->userid) {
9812 throw new portfolio_caller_exception('filenotfound');
9815 // Export either an area of files or a single file (see function for more detail).
9816 // The first arg is an id or null. If it is an id, the rest of the args are ignored.
9817 // If it is null, the rest of the args are used to load a list of files from get_areafiles.
9818 $this->set_file_and_format_data($this->fileid,
9819 $context->id,
9820 $this->component,
9821 $this->area,
9822 $this->sid,
9823 'timemodified',
9824 false);
9829 * Prepares the package up before control is passed to the portfolio plugin.
9831 * @throws portfolio_caller_exception
9832 * @return mixed
9834 public function prepare_package() {
9836 if ($this->plugin && $this->editor) {
9837 $options = portfolio_format_text_options();
9838 $context = context_module::instance($this->cmid);
9839 $options->context = $context;
9841 $plugin = $this->get_submission_plugin();
9843 $text = $plugin->get_editor_text($this->editor, $this->sid);
9844 $format = $plugin->get_editor_format($this->editor, $this->sid);
9846 $html = format_text($text, $format, $options);
9847 $html = portfolio_rewrite_pluginfile_urls($html,
9848 $context->id,
9849 'mod_assign',
9850 $this->area,
9851 $this->sid,
9852 $this->exporter->get('format'));
9854 $exporterclass = $this->exporter->get('formatclass');
9855 if (in_array($exporterclass, array(PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_RICHHTML))) {
9856 if ($files = $this->exporter->get('caller')->get('multifiles')) {
9857 foreach ($files as $file) {
9858 $this->exporter->copy_existing_file($file);
9861 return $this->exporter->write_new_file($html, 'assignment.html', !empty($files));
9862 } else if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
9863 $leapwriter = $this->exporter->get('format')->leap2a_writer();
9864 $entry = new portfolio_format_leap2a_entry($this->area . $this->cmid,
9865 $context->get_context_name(),
9866 'resource',
9867 $html);
9869 $entry->add_category('web', 'resource_type');
9870 $entry->author = $this->user;
9871 $leapwriter->add_entry($entry);
9872 if ($files = $this->exporter->get('caller')->get('multifiles')) {
9873 $leapwriter->link_files($entry, $files, $this->area . $this->cmid . 'file');
9874 foreach ($files as $file) {
9875 $this->exporter->copy_existing_file($file);
9878 return $this->exporter->write_new_file($leapwriter->to_xml(),
9879 $this->exporter->get('format')->manifest_name(),
9880 true);
9881 } else {
9882 debugging('invalid format class: ' . $this->exporter->get('formatclass'));
9887 if ($this->exporter->get('formatclass') == PORTFOLIO_FORMAT_LEAP2A) {
9888 $leapwriter = $this->exporter->get('format')->leap2a_writer();
9889 $files = array();
9890 if ($this->singlefile) {
9891 $files[] = $this->singlefile;
9892 } else if ($this->multifiles) {
9893 $files = $this->multifiles;
9894 } else {
9895 throw new portfolio_caller_exception('invalidpreparepackagefile',
9896 'portfolio',
9897 $this->get_return_url());
9900 $entryids = array();
9901 foreach ($files as $file) {
9902 $entry = new portfolio_format_leap2a_file($file->get_filename(), $file);
9903 $entry->author = $this->user;
9904 $leapwriter->add_entry($entry);
9905 $this->exporter->copy_existing_file($file);
9906 $entryids[] = $entry->id;
9908 if (count($files) > 1) {
9909 $baseid = 'assign' . $this->cmid . $this->area;
9910 $context = context_module::instance($this->cmid);
9912 // If we have multiple files, they should be grouped together into a folder.
9913 $entry = new portfolio_format_leap2a_entry($baseid . 'group',
9914 $context->get_context_name(),
9915 'selection');
9916 $leapwriter->add_entry($entry);
9917 $leapwriter->make_selection($entry, $entryids, 'Folder');
9919 return $this->exporter->write_new_file($leapwriter->to_xml(),
9920 $this->exporter->get('format')->manifest_name(),
9921 true);
9923 return $this->prepare_package_file();
9927 * Fetch the plugin by its type.
9929 * @return assign_submission_plugin
9931 protected function get_submission_plugin() {
9932 global $CFG;
9933 if (!$this->plugin || !$this->cmid) {
9934 return null;
9937 require_once($CFG->dirroot . '/mod/assign/locallib.php');
9939 $context = context_module::instance($this->cmid);
9941 $assignment = new assign($context, null, null);
9942 return $assignment->get_submission_plugin_by_type($this->plugin);
9946 * Calculate a sha1 has of either a single file or a list
9947 * of files based on the data set by load_data.
9949 * @return string
9951 public function get_sha1() {
9953 if ($this->plugin && $this->editor) {
9954 $plugin = $this->get_submission_plugin();
9955 $options = portfolio_format_text_options();
9956 $options->context = context_module::instance($this->cmid);
9958 $text = format_text($plugin->get_editor_text($this->editor, $this->sid),
9959 $plugin->get_editor_format($this->editor, $this->sid),
9960 $options);
9961 $textsha1 = sha1($text);
9962 $filesha1 = '';
9963 try {
9964 $filesha1 = $this->get_sha1_file();
9965 } catch (portfolio_caller_exception $e) {
9966 // No files.
9968 return sha1($textsha1 . $filesha1);
9970 return $this->get_sha1_file();
9974 * Calculate the time to transfer either a single file or a list
9975 * of files based on the data set by load_data.
9977 * @return int
9979 public function expected_time() {
9980 return $this->expected_time_file();
9984 * Checking the permissions.
9986 * @return bool
9988 public function check_permissions() {
9989 $context = context_module::instance($this->cmid);
9990 return has_capability('mod/assign:exportownsubmission', $context);
9994 * Display a module name.
9996 * @return string
9998 public static function display_name() {
9999 return get_string('modulename', 'assign');
10003 * Return array of formats supported by this portfolio call back.
10005 * @return array
10007 public static function base_supported_formats() {
10008 return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_LEAP2A);
10013 * Logic to happen when a/some group(s) has/have been deleted in a course.
10015 * @param int $courseid The course ID.
10016 * @param int $groupid The group id if it is known
10017 * @return void
10019 function assign_process_group_deleted_in_course($courseid, $groupid = null) {
10020 global $DB;
10022 $params = array('courseid' => $courseid);
10023 if ($groupid) {
10024 $params['groupid'] = $groupid;
10025 // We just update the group that was deleted.
10026 $sql = "SELECT o.id, o.assignid, o.groupid
10027 FROM {assign_overrides} o
10028 JOIN {assign} assign ON assign.id = o.assignid
10029 WHERE assign.course = :courseid
10030 AND o.groupid = :groupid";
10031 } else {
10032 // No groupid, we update all orphaned group overrides for all assign in course.
10033 $sql = "SELECT o.id, o.assignid, o.groupid
10034 FROM {assign_overrides} o
10035 JOIN {assign} assign ON assign.id = o.assignid
10036 LEFT JOIN {groups} grp ON grp.id = o.groupid
10037 WHERE assign.course = :courseid
10038 AND o.groupid IS NOT NULL
10039 AND grp.id IS NULL";
10041 $records = $DB->get_records_sql($sql, $params);
10042 if (!$records) {
10043 return; // Nothing to do.
10045 $DB->delete_records_list('assign_overrides', 'id', array_keys($records));
10046 $cache = cache::make('mod_assign', 'overrides');
10047 foreach ($records as $record) {
10048 $cache->delete("{$record->assignid}_g_{$record->groupid}");
10053 * Change the sort order of an override
10055 * @param int $id of the override
10056 * @param string $move direction of move
10057 * @param int $assignid of the assignment
10058 * @return bool success of operation
10060 function move_group_override($id, $move, $assignid) {
10061 global $DB;
10063 // Get the override object.
10064 if (!$override = $DB->get_record('assign_overrides', ['id' => $id, 'assignid' => $assignid], 'id, sortorder, groupid')) {
10065 return false;
10067 // Count the number of group overrides.
10068 $overridecountgroup = $DB->count_records('assign_overrides', array('userid' => null, 'assignid' => $assignid));
10070 // Calculate the new sortorder.
10071 if ( ($move == 'up') and ($override->sortorder > 1)) {
10072 $neworder = $override->sortorder - 1;
10073 } else if (($move == 'down') and ($override->sortorder < $overridecountgroup)) {
10074 $neworder = $override->sortorder + 1;
10075 } else {
10076 return false;
10079 // Retrieve the override object that is currently residing in the new position.
10080 $params = ['sortorder' => $neworder, 'assignid' => $assignid];
10081 if ($swapoverride = $DB->get_record('assign_overrides', $params, 'id, sortorder, groupid')) {
10083 // Swap the sortorders.
10084 $swapoverride->sortorder = $override->sortorder;
10085 $override->sortorder = $neworder;
10087 // Update the override records.
10088 $DB->update_record('assign_overrides', $override);
10089 $DB->update_record('assign_overrides', $swapoverride);
10091 // Delete cache for the 2 records we updated above.
10092 $cache = cache::make('mod_assign', 'overrides');
10093 $cache->delete("{$assignid}_g_{$override->groupid}");
10094 $cache->delete("{$assignid}_g_{$swapoverride->groupid}");
10097 reorder_group_overrides($assignid);
10098 return true;
10102 * Reorder the overrides starting at the override at the given startorder.
10104 * @param int $assignid of the assigment
10106 function reorder_group_overrides($assignid) {
10107 global $DB;
10109 $i = 1;
10110 if ($overrides = $DB->get_records('assign_overrides', array('userid' => null, 'assignid' => $assignid), 'sortorder ASC')) {
10111 $cache = cache::make('mod_assign', 'overrides');
10112 foreach ($overrides as $override) {
10113 $f = new stdClass();
10114 $f->id = $override->id;
10115 $f->sortorder = $i++;
10116 $DB->update_record('assign_overrides', $f);
10117 $cache->delete("{$assignid}_g_{$override->groupid}");
10119 // Update priorities of group overrides.
10120 $params = [
10121 'modulename' => 'assign',
10122 'instance' => $override->assignid,
10123 'groupid' => $override->groupid
10125 $DB->set_field('event', 'priority', $f->sortorder, $params);
10131 * Get the information about the standard assign JavaScript module.
10132 * @return array a standard jsmodule structure.
10134 function assign_get_js_module() {
10135 return array(
10136 'name' => 'mod_assign',
10137 'fullpath' => '/mod/assign/module.js',