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