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