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