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