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