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