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