Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 38 and 311] [Versions 39 and 311]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * This file contains the definition for the class assignment 19 * 20 * This class provides all the functionality for the new assign module. 21 * 22 * @package mod_assign 23 * @copyright 2012 NetSpot {@link http://www.netspot.com.au} 24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 */ 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 // Assignment submission statuses. 30 define('ASSIGN_SUBMISSION_STATUS_NEW', 'new'); 31 define('ASSIGN_SUBMISSION_STATUS_REOPENED', 'reopened'); 32 define('ASSIGN_SUBMISSION_STATUS_DRAFT', 'draft'); 33 define('ASSIGN_SUBMISSION_STATUS_SUBMITTED', 'submitted'); 34 35 // Search filters for grading page. 36 define('ASSIGN_FILTER_NONE', 'none'); 37 define('ASSIGN_FILTER_SUBMITTED', 'submitted'); 38 define('ASSIGN_FILTER_NOT_SUBMITTED', 'notsubmitted'); 39 define('ASSIGN_FILTER_SINGLE_USER', 'singleuser'); 40 define('ASSIGN_FILTER_REQUIRE_GRADING', 'requiregrading'); 41 define('ASSIGN_FILTER_GRANTED_EXTENSION', 'grantedextension'); 42 define('ASSIGN_FILTER_DRAFT', 'draft'); 43 44 // Marker filter for grading page. 45 define('ASSIGN_MARKER_FILTER_NO_MARKER', -1); 46 47 // Reopen attempt methods. 48 define('ASSIGN_ATTEMPT_REOPEN_METHOD_NONE', 'none'); 49 define('ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL', 'manual'); 50 define('ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS', 'untilpass'); 51 52 // Special value means allow unlimited attempts. 53 define('ASSIGN_UNLIMITED_ATTEMPTS', -1); 54 55 // Special value means no grade has been set. 56 define('ASSIGN_GRADE_NOT_SET', -1); 57 58 // Grading states. 59 define('ASSIGN_GRADING_STATUS_GRADED', 'graded'); 60 define('ASSIGN_GRADING_STATUS_NOT_GRADED', 'notgraded'); 61 62 // Marking workflow states. 63 define('ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED', 'notmarked'); 64 define('ASSIGN_MARKING_WORKFLOW_STATE_INMARKING', 'inmarking'); 65 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW', 'readyforreview'); 66 define('ASSIGN_MARKING_WORKFLOW_STATE_INREVIEW', 'inreview'); 67 define('ASSIGN_MARKING_WORKFLOW_STATE_READYFORRELEASE', 'readyforrelease'); 68 define('ASSIGN_MARKING_WORKFLOW_STATE_RELEASED', 'released'); 69 70 /** ASSIGN_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */ 71 define("ASSIGN_MAX_EVENT_LENGTH", "432000"); 72 73 // Name of file area for intro attachments. 74 define('ASSIGN_INTROATTACHMENT_FILEAREA', 'introattachment'); 75 76 // Event types. 77 define('ASSIGN_EVENT_TYPE_DUE', 'due'); 78 define('ASSIGN_EVENT_TYPE_GRADINGDUE', 'gradingdue'); 79 define('ASSIGN_EVENT_TYPE_OPEN', 'open'); 80 define('ASSIGN_EVENT_TYPE_CLOSE', 'close'); 81 82 require_once($CFG->libdir . '/accesslib.php'); 83 require_once($CFG->libdir . '/formslib.php'); 84 require_once($CFG->dirroot . '/repository/lib.php'); 85 require_once($CFG->dirroot . '/mod/assign/mod_form.php'); 86 require_once($CFG->libdir . '/gradelib.php'); 87 require_once($CFG->dirroot . '/grade/grading/lib.php'); 88 require_once($CFG->dirroot . '/mod/assign/feedbackplugin.php'); 89 require_once($CFG->dirroot . '/mod/assign/submissionplugin.php'); 90 require_once($CFG->dirroot . '/mod/assign/renderable.php'); 91 require_once($CFG->dirroot . '/mod/assign/gradingtable.php'); 92 require_once($CFG->libdir . '/portfolio/caller.php'); 93 94 use \mod_assign\output\grading_app; 95 96 /** 97 * Standard base class for mod_assign (assignment types). 98 * 99 * @package mod_assign 100 * @copyright 2012 NetSpot {@link http://www.netspot.com.au} 101 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 102 */ 103 class assign { 104 105 /** @var stdClass the assignment record that contains the global settings for this assign instance */ 106 private $instance; 107 108 /** @var array $var array an array containing per-user assignment records, each having calculated properties (e.g. dates) */ 109 private $userinstances = []; 110 111 /** @var grade_item the grade_item record for this assign instance's primary grade item. */ 112 private $gradeitem; 113 114 /** @var context the context of the course module for this assign instance 115 * (or just the course if we are creating a new one) 116 */ 117 private $context; 118 119 /** @var stdClass the course this assign instance belongs to */ 120 private $course; 121 122 /** @var stdClass the admin config for all assign instances */ 123 private $adminconfig; 124 125 /** @var assign_renderer the custom renderer for this module */ 126 private $output; 127 128 /** @var cm_info the course module for this assign instance */ 129 private $coursemodule; 130 131 /** @var array cache for things like the coursemodule name or the scale menu - 132 * only lives for a single request. 133 */ 134 private $cache; 135 136 /** @var array list of the installed submission plugins */ 137 private $submissionplugins; 138 139 /** @var array list of the installed feedback plugins */ 140 private $feedbackplugins; 141 142 /** @var string action to be used to return to this page 143 * (without repeating any form submissions etc). 144 */ 145 private $returnaction = 'view'; 146 147 /** @var array params to be used to return to this page */ 148 private $returnparams = array(); 149 150 /** @var string modulename prevents excessive calls to get_string */ 151 private static $modulename = null; 152 153 /** @var string modulenameplural prevents excessive calls to get_string */ 154 private static $modulenameplural = null; 155 156 /** @var array of marking workflow states for the current user */ 157 private $markingworkflowstates = null; 158 159 /** @var bool whether to exclude users with inactive enrolment */ 160 private $showonlyactiveenrol = null; 161 162 /** @var string A key used to identify userlists created by this object. */ 163 private $useridlistid = null; 164 165 /** @var array cached list of participants for this assignment. The cache key will be group, showactive and the context id */ 166 private $participants = array(); 167 168 /** @var array cached list of user groups when team submissions are enabled. The cache key will be the user. */ 169 private $usersubmissiongroups = array(); 170 171 /** @var array cached list of user groups. The cache key will be the user. */ 172 private $usergroups = array(); 173 174 /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */ 175 private $sharedgroupmembers = array(); 176 177 /** 178 * @var stdClass The most recent team submission. Used to determine additional attempt numbers and whether 179 * to update the gradebook. 180 */ 181 private $mostrecentteamsubmission = null; 182 183 /** @var array Array of error messages encountered during the execution of assignment related operations. */ 184 private $errors = array(); 185 186 /** 187 * Constructor for the base assign class. 188 * 189 * Note: For $coursemodule you can supply a stdclass if you like, but it 190 * will be more efficient to supply a cm_info object. 191 * 192 * @param mixed $coursemodulecontext context|null the course module context 193 * (or the course context if the coursemodule has not been 194 * created yet). 195 * @param mixed $coursemodule the current course module if it was already loaded, 196 * otherwise this class will load one from the context as required. 197 * @param mixed $course the current course if it was already loaded, 198 * otherwise this class will load one from the context as required. 199 */ 200 public function __construct($coursemodulecontext, $coursemodule, $course) { 201 global $SESSION; 202 203 $this->context = $coursemodulecontext; 204 $this->course = $course; 205 206 // Ensure that $this->coursemodule is a cm_info object (or null). 207 $this->coursemodule = cm_info::create($coursemodule); 208 209 // Temporary cache only lives for a single request - used to reduce db lookups. 210 $this->cache = array(); 211 212 $this->submissionplugins = $this->load_plugins('assignsubmission'); 213 $this->feedbackplugins = $this->load_plugins('assignfeedback'); 214 215 // Extra entropy is required for uniqid() to work on cygwin. 216 $this->useridlistid = clean_param(uniqid('', true), PARAM_ALPHANUM); 217 218 if (!isset($SESSION->mod_assign_useridlist)) { 219 $SESSION->mod_assign_useridlist = []; 220 } 221 } 222 223 /** 224 * Set the action and parameters that can be used to return to the current page. 225 * 226 * @param string $action The action for the current page 227 * @param array $params An array of name value pairs which form the parameters 228 * to return to the current page. 229 * @return void 230 */ 231 public function register_return_link($action, $params) { 232 global $PAGE; 233 $params['action'] = $action; 234 $cm = $this->get_course_module(); 235 if ($cm) { 236 $currenturl = new moodle_url('/mod/assign/view.php', array('id' => $cm->id)); 237 } else { 238 $currenturl = new moodle_url('/mod/assign/index.php', array('id' => $this->get_course()->id)); 239 } 240 241 $currenturl->params($params); 242 $PAGE->set_url($currenturl); 243 } 244 245 /** 246 * Return an action that can be used to get back to the current page. 247 * 248 * @return string action 249 */ 250 public function get_return_action() { 251 global $PAGE; 252 253 // Web services don't set a URL, we should avoid debugging when ussing the url object. 254 if (!WS_SERVER) { 255 $params = $PAGE->url->params(); 256 } 257 258 if (!empty($params['action'])) { 259 return $params['action']; 260 } 261 return ''; 262 } 263 264 /** 265 * Based on the current assignment settings should we display the intro. 266 * 267 * @return bool showintro 268 */ 269 public function show_intro() { 270 if ($this->get_instance()->alwaysshowdescription || 271 time() > $this->get_instance()->allowsubmissionsfromdate) { 272 return true; 273 } 274 return false; 275 } 276 277 /** 278 * Return a list of parameters that can be used to get back to the current page. 279 * 280 * @return array params 281 */ 282 public function get_return_params() { 283 global $PAGE; 284 285 $params = array(); 286 if (!WS_SERVER) { 287 $params = $PAGE->url->params(); 288 } 289 unset($params['id']); 290 unset($params['action']); 291 return $params; 292 } 293 294 /** 295 * Set the submitted form data. 296 * 297 * @param stdClass $data The form data (instance) 298 */ 299 public function set_instance(stdClass $data) { 300 $this->instance = $data; 301 } 302 303 /** 304 * Set the context. 305 * 306 * @param context $context The new context 307 */ 308 public function set_context(context $context) { 309 $this->context = $context; 310 } 311 312 /** 313 * Set the course data. 314 * 315 * @param stdClass $course The course data 316 */ 317 public function set_course(stdClass $course) { 318 $this->course = $course; 319 } 320 321 /** 322 * Set error message. 323 * 324 * @param string $message The error message 325 */ 326 protected function set_error_message(string $message) { 327 $this->errors[] = $message; 328 } 329 330 /** 331 * Get error messages. 332 * 333 * @return array The array of error messages 334 */ 335 protected function get_error_messages(): array { 336 return $this->errors; 337 } 338 339 /** 340 * Get list of feedback plugins installed. 341 * 342 * @return array 343 */ 344 public function get_feedback_plugins() { 345 return $this->feedbackplugins; 346 } 347 348 /** 349 * Get list of submission plugins installed. 350 * 351 * @return array 352 */ 353 public function get_submission_plugins() { 354 return $this->submissionplugins; 355 } 356 357 /** 358 * Is blind marking enabled and reveal identities not set yet? 359 * 360 * @return bool 361 */ 362 public function is_blind_marking() { 363 return $this->get_instance()->blindmarking && !$this->get_instance()->revealidentities; 364 } 365 366 /** 367 * Is hidden grading enabled? 368 * 369 * This just checks the assignment settings. Remember to check 370 * the user has the 'showhiddengrader' capability too 371 * 372 * @return bool 373 */ 374 public function is_hidden_grader() { 375 return $this->get_instance()->hidegrader; 376 } 377 378 /** 379 * Does an assignment have submission(s) or grade(s) already? 380 * 381 * @return bool 382 */ 383 public function has_submissions_or_grades() { 384 $allgrades = $this->count_grades(); 385 $allsubmissions = $this->count_submissions(); 386 if (($allgrades == 0) && ($allsubmissions == 0)) { 387 return false; 388 } 389 return true; 390 } 391 392 /** 393 * Get a specific submission plugin by its type. 394 * 395 * @param string $subtype assignsubmission | assignfeedback 396 * @param string $type 397 * @return mixed assign_plugin|null 398 */ 399 public function get_plugin_by_type($subtype, $type) { 400 $shortsubtype = substr($subtype, strlen('assign')); 401 $name = $shortsubtype . 'plugins'; 402 if ($name != 'feedbackplugins' && $name != 'submissionplugins') { 403 return null; 404 } 405 $pluginlist = $this->$name; 406 foreach ($pluginlist as $plugin) { 407 if ($plugin->get_type() == $type) { 408 return $plugin; 409 } 410 } 411 return null; 412 } 413 414 /** 415 * Get a feedback plugin by type. 416 * 417 * @param string $type - The type of plugin e.g comments 418 * @return mixed assign_feedback_plugin|null 419 */ 420 public function get_feedback_plugin_by_type($type) { 421 return $this->get_plugin_by_type('assignfeedback', $type); 422 } 423 424 /** 425 * Get a submission plugin by type. 426 * 427 * @param string $type - The type of plugin e.g comments 428 * @return mixed assign_submission_plugin|null 429 */ 430 public function get_submission_plugin_by_type($type) { 431 return $this->get_plugin_by_type('assignsubmission', $type); 432 } 433 434 /** 435 * Load the plugins from the sub folders under subtype. 436 * 437 * @param string $subtype - either submission or feedback 438 * @return array - The sorted list of plugins 439 */ 440 public function load_plugins($subtype) { 441 global $CFG; 442 $result = array(); 443 444 $names = core_component::get_plugin_list($subtype); 445 446 foreach ($names as $name => $path) { 447 if (file_exists($path . '/locallib.php')) { 448 require_once ($path . '/locallib.php'); 449 450 $shortsubtype = substr($subtype, strlen('assign')); 451 $pluginclass = 'assign_' . $shortsubtype . '_' . $name; 452 453 $plugin = new $pluginclass($this, $name); 454 455 if ($plugin instanceof assign_plugin) { 456 $idx = $plugin->get_sort_order(); 457 while (array_key_exists($idx, $result)) { 458 $idx +=1; 459 } 460 $result[$idx] = $plugin; 461 } 462 } 463 } 464 ksort($result); 465 return $result; 466 } 467 468 /** 469 * Display the assignment, used by view.php 470 * 471 * The assignment is displayed differently depending on your role, 472 * the settings for the assignment and the status of the assignment. 473 * 474 * @param string $action The current action if any. 475 * @param array $args Optional arguments to pass to the view (instead of getting them from GET and POST). 476 * @return string - The page output. 477 */ 478 public function view($action='', $args = array()) { 479 global $PAGE; 480 481 $o = ''; 482 $mform = null; 483 $notices = array(); 484 $nextpageparams = array(); 485 486 if (!empty($this->get_course_module()->id)) { 487 $nextpageparams['id'] = $this->get_course_module()->id; 488 } 489 490 // Handle form submissions first. 491 if ($action == 'savesubmission') { 492 $action = 'editsubmission'; 493 if ($this->process_save_submission($mform, $notices)) { 494 $action = 'redirect'; 495 if ($this->can_grade()) { 496 $nextpageparams['action'] = 'grading'; 497 } else { 498 $nextpageparams['action'] = 'view'; 499 } 500 } 501 } else if ($action == 'editprevioussubmission') { 502 $action = 'editsubmission'; 503 if ($this->process_copy_previous_attempt($notices)) { 504 $action = 'redirect'; 505 $nextpageparams['action'] = 'editsubmission'; 506 } 507 } else if ($action == 'lock') { 508 $this->process_lock_submission(); 509 $action = 'redirect'; 510 $nextpageparams['action'] = 'grading'; 511 } else if ($action == 'removesubmission') { 512 $this->process_remove_submission(); 513 $action = 'redirect'; 514 if ($this->can_grade()) { 515 $nextpageparams['action'] = 'grading'; 516 } else { 517 $nextpageparams['action'] = 'view'; 518 } 519 } else if ($action == 'addattempt') { 520 $this->process_add_attempt(required_param('userid', PARAM_INT)); 521 $action = 'redirect'; 522 $nextpageparams['action'] = 'grading'; 523 } else if ($action == 'reverttodraft') { 524 $this->process_revert_to_draft(); 525 $action = 'redirect'; 526 $nextpageparams['action'] = 'grading'; 527 } else if ($action == 'unlock') { 528 $this->process_unlock_submission(); 529 $action = 'redirect'; 530 $nextpageparams['action'] = 'grading'; 531 } else if ($action == 'setbatchmarkingworkflowstate') { 532 $this->process_set_batch_marking_workflow_state(); 533 $action = 'redirect'; 534 $nextpageparams['action'] = 'grading'; 535 } else if ($action == 'setbatchmarkingallocation') { 536 $this->process_set_batch_marking_allocation(); 537 $action = 'redirect'; 538 $nextpageparams['action'] = 'grading'; 539 } else if ($action == 'confirmsubmit') { 540 $action = 'submit'; 541 if ($this->process_submit_for_grading($mform, $notices)) { 542 $action = 'redirect'; 543 $nextpageparams['action'] = 'view'; 544 } else if ($notices) { 545 $action = 'viewsubmitforgradingerror'; 546 } 547 } else if ($action == 'submitotherforgrading') { 548 if ($this->process_submit_other_for_grading($mform, $notices)) { 549 $action = 'redirect'; 550 $nextpageparams['action'] = 'grading'; 551 } else { 552 $action = 'viewsubmitforgradingerror'; 553 } 554 } else if ($action == 'gradingbatchoperation') { 555 $action = $this->process_grading_batch_operation($mform); 556 if ($action == 'grading') { 557 $action = 'redirect'; 558 $nextpageparams['action'] = 'grading'; 559 } 560 } else if ($action == 'submitgrade') { 561 if (optional_param('saveandshownext', null, PARAM_RAW)) { 562 // Save and show next. 563 $action = 'grade'; 564 if ($this->process_save_grade($mform)) { 565 $action = 'redirect'; 566 $nextpageparams['action'] = 'grade'; 567 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1; 568 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM); 569 } 570 } else if (optional_param('nosaveandprevious', null, PARAM_RAW)) { 571 $action = 'redirect'; 572 $nextpageparams['action'] = 'grade'; 573 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) - 1; 574 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM); 575 } else if (optional_param('nosaveandnext', null, PARAM_RAW)) { 576 $action = 'redirect'; 577 $nextpageparams['action'] = 'grade'; 578 $nextpageparams['rownum'] = optional_param('rownum', 0, PARAM_INT) + 1; 579 $nextpageparams['useridlistid'] = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM); 580 } else if (optional_param('savegrade', null, PARAM_RAW)) { 581 // Save changes button. 582 $action = 'grade'; 583 if ($this->process_save_grade($mform)) { 584 $action = 'redirect'; 585 $nextpageparams['action'] = 'savegradingresult'; 586 } 587 } else { 588 // Cancel button. 589 $action = 'redirect'; 590 $nextpageparams['action'] = 'grading'; 591 } 592 } else if ($action == 'quickgrade') { 593 $message = $this->process_save_quick_grades(); 594 $action = 'quickgradingresult'; 595 } else if ($action == 'saveoptions') { 596 $this->process_save_grading_options(); 597 $action = 'redirect'; 598 $nextpageparams['action'] = 'grading'; 599 } else if ($action == 'saveextension') { 600 $action = 'grantextension'; 601 if ($this->process_save_extension($mform)) { 602 $action = 'redirect'; 603 $nextpageparams['action'] = 'grading'; 604 } 605 } else if ($action == 'revealidentitiesconfirm') { 606 $this->process_reveal_identities(); 607 $action = 'redirect'; 608 $nextpageparams['action'] = 'grading'; 609 } 610 611 $returnparams = array('rownum'=>optional_param('rownum', 0, PARAM_INT), 612 'useridlistid' => optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM)); 613 $this->register_return_link($action, $returnparams); 614 615 // Include any page action as part of the body tag CSS id. 616 if (!empty($action)) { 617 $PAGE->set_pagetype('mod-assign-' . $action); 618 } 619 // Now show the right view page. 620 if ($action == 'redirect') { 621 $nextpageurl = new moodle_url('/mod/assign/view.php', $nextpageparams); 622 $messages = ''; 623 $messagetype = \core\output\notification::NOTIFY_INFO; 624 $errors = $this->get_error_messages(); 625 if (!empty($errors)) { 626 $messages = html_writer::alist($errors, ['class' => 'mb-1 mt-1']); 627 $messagetype = \core\output\notification::NOTIFY_ERROR; 628 } 629 redirect($nextpageurl, $messages, null, $messagetype); 630 return; 631 } else if ($action == 'savegradingresult') { 632 $message = get_string('gradingchangessaved', 'assign'); 633 $o .= $this->view_savegrading_result($message); 634 } else if ($action == 'quickgradingresult') { 635 $mform = null; 636 $o .= $this->view_quickgrading_result($message); 637 } else if ($action == 'gradingpanel') { 638 $o .= $this->view_single_grading_panel($args); 639 } else if ($action == 'grade') { 640 $o .= $this->view_single_grade_page($mform); 641 } else if ($action == 'viewpluginassignfeedback') { 642 $o .= $this->view_plugin_content('assignfeedback'); 643 } else if ($action == 'viewpluginassignsubmission') { 644 $o .= $this->view_plugin_content('assignsubmission'); 645 } else if ($action == 'editsubmission') { 646 $o .= $this->view_edit_submission_page($mform, $notices); 647 } else if ($action == 'grader') { 648 $o .= $this->view_grader(); 649 } else if ($action == 'grading') { 650 $o .= $this->view_grading_page(); 651 } else if ($action == 'downloadall') { 652 $o .= $this->download_submissions(); 653 } else if ($action == 'submit') { 654 $o .= $this->check_submit_for_grading($mform); 655 } else if ($action == 'grantextension') { 656 $o .= $this->view_grant_extension($mform); 657 } else if ($action == 'revealidentities') { 658 $o .= $this->view_reveal_identities_confirm($mform); 659 } else if ($action == 'removesubmissionconfirm') { 660 $o .= $this->view_remove_submission_confirm(); 661 } else if ($action == 'plugingradingbatchoperation') { 662 $o .= $this->view_plugin_grading_batch_operation($mform); 663 } else if ($action == 'viewpluginpage') { 664 $o .= $this->view_plugin_page(); 665 } else if ($action == 'viewcourseindex') { 666 $o .= $this->view_course_index(); 667 } else if ($action == 'viewbatchsetmarkingworkflowstate') { 668 $o .= $this->view_batch_set_workflow_state($mform); 669 } else if ($action == 'viewbatchmarkingallocation') { 670 $o .= $this->view_batch_markingallocation($mform); 671 } else if ($action == 'viewsubmitforgradingerror') { 672 $o .= $this->view_error_page(get_string('submitforgrading', 'assign'), $notices); 673 } else if ($action == 'fixrescalednullgrades') { 674 $o .= $this->view_fix_rescaled_null_grades(); 675 } else { 676 $o .= $this->view_submission_page(); 677 } 678 679 return $o; 680 } 681 682 /** 683 * Add this instance to the database. 684 * 685 * @param stdClass $formdata The data submitted from the form 686 * @param bool $callplugins This is used to skip the plugin code 687 * when upgrading an old assignment to a new one (the plugins get called manually) 688 * @return mixed false if an error occurs or the int id of the new instance 689 */ 690 public function add_instance(stdClass $formdata, $callplugins) { 691 global $DB; 692 $adminconfig = $this->get_admin_config(); 693 694 $err = ''; 695 696 // Add the database record. 697 $update = new stdClass(); 698 $update->name = $formdata->name; 699 $update->timemodified = time(); 700 $update->timecreated = time(); 701 $update->course = $formdata->course; 702 $update->courseid = $formdata->course; 703 $update->intro = $formdata->intro; 704 $update->introformat = $formdata->introformat; 705 $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription); 706 $update->submissiondrafts = $formdata->submissiondrafts; 707 $update->requiresubmissionstatement = $formdata->requiresubmissionstatement; 708 $update->sendnotifications = $formdata->sendnotifications; 709 $update->sendlatenotifications = $formdata->sendlatenotifications; 710 $update->sendstudentnotifications = $adminconfig->sendstudentnotifications; 711 if (isset($formdata->sendstudentnotifications)) { 712 $update->sendstudentnotifications = $formdata->sendstudentnotifications; 713 } 714 $update->duedate = $formdata->duedate; 715 $update->cutoffdate = $formdata->cutoffdate; 716 $update->gradingduedate = $formdata->gradingduedate; 717 $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate; 718 $update->grade = $formdata->grade; 719 $update->completionsubmit = !empty($formdata->completionsubmit); 720 $update->teamsubmission = $formdata->teamsubmission; 721 $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit; 722 if (isset($formdata->teamsubmissiongroupingid)) { 723 $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid; 724 } 725 $update->blindmarking = $formdata->blindmarking; 726 if (isset($formdata->hidegrader)) { 727 $update->hidegrader = $formdata->hidegrader; 728 } 729 $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE; 730 if (!empty($formdata->attemptreopenmethod)) { 731 $update->attemptreopenmethod = $formdata->attemptreopenmethod; 732 } 733 if (!empty($formdata->maxattempts)) { 734 $update->maxattempts = $formdata->maxattempts; 735 } 736 if (isset($formdata->preventsubmissionnotingroup)) { 737 $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup; 738 } 739 $update->markingworkflow = $formdata->markingworkflow; 740 $update->markingallocation = $formdata->markingallocation; 741 if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled. 742 $update->markingallocation = 0; 743 } 744 745 $returnid = $DB->insert_record('assign', $update); 746 $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST); 747 // Cache the course record. 748 $this->course = $DB->get_record('course', array('id'=>$formdata->course), '*', MUST_EXIST); 749 750 $this->save_intro_draft_files($formdata); 751 752 if ($callplugins) { 753 // Call save_settings hook for submission plugins. 754 foreach ($this->submissionplugins as $plugin) { 755 if (!$this->update_plugin_instance($plugin, $formdata)) { 756 print_error($plugin->get_error()); 757 return false; 758 } 759 } 760 foreach ($this->feedbackplugins as $plugin) { 761 if (!$this->update_plugin_instance($plugin, $formdata)) { 762 print_error($plugin->get_error()); 763 return false; 764 } 765 } 766 767 // In the case of upgrades the coursemodule has not been set, 768 // so we need to wait before calling these two. 769 $this->update_calendar($formdata->coursemodule); 770 if (!empty($formdata->completionexpected)) { 771 \core_completion\api::update_completion_date_event($formdata->coursemodule, 'assign', $this->instance, 772 $formdata->completionexpected); 773 } 774 $this->update_gradebook(false, $formdata->coursemodule); 775 776 } 777 778 $update = new stdClass(); 779 $update->id = $this->get_instance()->id; 780 $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0; 781 $DB->update_record('assign', $update); 782 783 return $returnid; 784 } 785 786 /** 787 * Delete all grades from the gradebook for this assignment. 788 * 789 * @return bool 790 */ 791 protected function delete_grades() { 792 global $CFG; 793 794 $result = grade_update('mod/assign', 795 $this->get_course()->id, 796 'mod', 797 'assign', 798 $this->get_instance()->id, 799 0, 800 null, 801 array('deleted'=>1)); 802 return $result == GRADE_UPDATE_OK; 803 } 804 805 /** 806 * Delete this instance from the database. 807 * 808 * @return bool false if an error occurs 809 */ 810 public function delete_instance() { 811 global $DB; 812 $result = true; 813 814 foreach ($this->submissionplugins as $plugin) { 815 if (!$plugin->delete_instance()) { 816 print_error($plugin->get_error()); 817 $result = false; 818 } 819 } 820 foreach ($this->feedbackplugins as $plugin) { 821 if (!$plugin->delete_instance()) { 822 print_error($plugin->get_error()); 823 $result = false; 824 } 825 } 826 827 // Delete files associated with this assignment. 828 $fs = get_file_storage(); 829 if (! $fs->delete_area_files($this->context->id) ) { 830 $result = false; 831 } 832 833 $this->delete_all_overrides(); 834 835 // Delete_records will throw an exception if it fails - so no need for error checking here. 836 $DB->delete_records('assign_submission', array('assignment' => $this->get_instance()->id)); 837 $DB->delete_records('assign_grades', array('assignment' => $this->get_instance()->id)); 838 $DB->delete_records('assign_plugin_config', array('assignment' => $this->get_instance()->id)); 839 $DB->delete_records('assign_user_flags', array('assignment' => $this->get_instance()->id)); 840 $DB->delete_records('assign_user_mapping', array('assignment' => $this->get_instance()->id)); 841 842 // Delete items from the gradebook. 843 if (! $this->delete_grades()) { 844 $result = false; 845 } 846 847 // Delete the instance. 848 // We must delete the module record after we delete the grade item. 849 $DB->delete_records('assign', array('id'=>$this->get_instance()->id)); 850 851 return $result; 852 } 853 854 /** 855 * Deletes a assign override from the database and clears any corresponding calendar events 856 * 857 * @param int $overrideid The id of the override being deleted 858 * @return bool true on success 859 */ 860 public function delete_override($overrideid) { 861 global $CFG, $DB; 862 863 require_once($CFG->dirroot . '/calendar/lib.php'); 864 865 $cm = $this->get_course_module(); 866 if (empty($cm)) { 867 $instance = $this->get_instance(); 868 $cm = get_coursemodule_from_instance('assign', $instance->id, $instance->course); 869 } 870 871 $override = $DB->get_record('assign_overrides', array('id' => $overrideid), '*', MUST_EXIST); 872 873 // Delete the events. 874 $conds = array('modulename' => 'assign', 'instance' => $this->get_instance()->id); 875 if (isset($override->userid)) { 876 $conds['userid'] = $override->userid; 877 $cachekey = "{$cm->instance}_u_{$override->userid}"; 878 } else { 879 $conds['groupid'] = $override->groupid; 880 $cachekey = "{$cm->instance}_g_{$override->groupid}"; 881 } 882 $events = $DB->get_records('event', $conds); 883 foreach ($events as $event) { 884 $eventold = calendar_event::load($event); 885 $eventold->delete(); 886 } 887 888 $DB->delete_records('assign_overrides', array('id' => $overrideid)); 889 cache::make('mod_assign', 'overrides')->delete($cachekey); 890 891 // Set the common parameters for one of the events we will be triggering. 892 $params = array( 893 'objectid' => $override->id, 894 'context' => context_module::instance($cm->id), 895 'other' => array( 896 'assignid' => $override->assignid 897 ) 898 ); 899 // Determine which override deleted event to fire. 900 if (!empty($override->userid)) { 901 $params['relateduserid'] = $override->userid; 902 $event = \mod_assign\event\user_override_deleted::create($params); 903 } else { 904 $params['other']['groupid'] = $override->groupid; 905 $event = \mod_assign\event\group_override_deleted::create($params); 906 } 907 908 // Trigger the override deleted event. 909 $event->add_record_snapshot('assign_overrides', $override); 910 $event->trigger(); 911 912 return true; 913 } 914 915 /** 916 * Deletes all assign overrides from the database and clears any corresponding calendar events 917 */ 918 public function delete_all_overrides() { 919 global $DB; 920 921 $overrides = $DB->get_records('assign_overrides', array('assignid' => $this->get_instance()->id), 'id'); 922 foreach ($overrides as $override) { 923 $this->delete_override($override->id); 924 } 925 } 926 927 /** 928 * Updates the assign properties with override information for a user. 929 * 930 * Algorithm: For each assign setting, if there is a matching user-specific override, 931 * then use that otherwise, if there are group-specific overrides, return the most 932 * lenient combination of them. If neither applies, leave the assign setting unchanged. 933 * 934 * @param int $userid The userid. 935 */ 936 public function update_effective_access($userid) { 937 938 $override = $this->override_exists($userid); 939 940 // Merge with assign defaults. 941 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate'); 942 foreach ($keys as $key) { 943 if (isset($override->{$key})) { 944 $this->get_instance($userid)->{$key} = $override->{$key}; 945 } 946 } 947 948 } 949 950 /** 951 * Returns whether an assign has any overrides. 952 * 953 * @return true if any, false if not 954 */ 955 public function has_overrides() { 956 global $DB; 957 958 $override = $DB->record_exists('assign_overrides', array('assignid' => $this->get_instance()->id)); 959 960 if ($override) { 961 return true; 962 } 963 964 return false; 965 } 966 967 /** 968 * Returns user override 969 * 970 * Algorithm: For each assign setting, if there is a matching user-specific override, 971 * then use that otherwise, if there are group-specific overrides, use the one with the 972 * lowest sort order. If neither applies, leave the assign setting unchanged. 973 * 974 * @param int $userid The userid. 975 * @return stdClass The override 976 */ 977 public function override_exists($userid) { 978 global $DB; 979 980 // Gets an assoc array containing the keys for defined user overrides only. 981 $getuseroverride = function($userid) use ($DB) { 982 $useroverride = $DB->get_record('assign_overrides', ['assignid' => $this->get_instance()->id, 'userid' => $userid]); 983 return $useroverride ? get_object_vars($useroverride) : []; 984 }; 985 986 // Gets an assoc array containing the keys for defined group overrides only. 987 $getgroupoverride = function($userid) use ($DB) { 988 $groupings = groups_get_user_groups($this->get_instance()->course, $userid); 989 990 if (empty($groupings[0])) { 991 return []; 992 } 993 994 // Select all overrides that apply to the User's groups. 995 list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0])); 996 $sql = "SELECT * FROM {assign_overrides} 997 WHERE groupid $extra AND assignid = ? ORDER BY sortorder ASC"; 998 $params[] = $this->get_instance()->id; 999 $groupoverride = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE); 1000 1001 return $groupoverride ? get_object_vars($groupoverride) : []; 1002 }; 1003 1004 // Later arguments clobber earlier ones with array_merge. The two helper functions 1005 // return arrays containing keys for only the defined overrides. So we get the 1006 // desired behaviour as per the algorithm. 1007 return (object)array_merge( 1008 ['duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null], 1009 $getgroupoverride($userid), 1010 $getuseroverride($userid) 1011 ); 1012 } 1013 1014 /** 1015 * Check if the given calendar_event is either a user or group override 1016 * event. 1017 * 1018 * @return bool 1019 */ 1020 public function is_override_calendar_event(\calendar_event $event) { 1021 global $DB; 1022 1023 if (!isset($event->modulename)) { 1024 return false; 1025 } 1026 1027 if ($event->modulename != 'assign') { 1028 return false; 1029 } 1030 1031 if (!isset($event->instance)) { 1032 return false; 1033 } 1034 1035 if (!isset($event->userid) && !isset($event->groupid)) { 1036 return false; 1037 } 1038 1039 $overrideparams = [ 1040 'assignid' => $event->instance 1041 ]; 1042 1043 if (isset($event->groupid)) { 1044 $overrideparams['groupid'] = $event->groupid; 1045 } else if (isset($event->userid)) { 1046 $overrideparams['userid'] = $event->userid; 1047 } 1048 1049 if ($DB->get_record('assign_overrides', $overrideparams)) { 1050 return true; 1051 } else { 1052 return false; 1053 } 1054 } 1055 1056 /** 1057 * This function calculates the minimum and maximum cutoff values for the timestart of 1058 * the given event. 1059 * 1060 * It will return an array with two values, the first being the minimum cutoff value and 1061 * the second being the maximum cutoff value. Either or both values can be null, which 1062 * indicates there is no minimum or maximum, respectively. 1063 * 1064 * If a cutoff is required then the function must return an array containing the cutoff 1065 * timestamp and error string to display to the user if the cutoff value is violated. 1066 * 1067 * A minimum and maximum cutoff return value will look like: 1068 * [ 1069 * [1505704373, 'The due date must be after the sbumission start date'], 1070 * [1506741172, 'The due date must be before the cutoff date'] 1071 * ] 1072 * 1073 * If the event does not have a valid timestart range then [false, false] will 1074 * be returned. 1075 * 1076 * @param calendar_event $event The calendar event to get the time range for 1077 * @return array 1078 */ 1079 function get_valid_calendar_event_timestart_range(\calendar_event $event) { 1080 $instance = $this->get_instance(); 1081 $submissionsfromdate = $instance->allowsubmissionsfromdate; 1082 $cutoffdate = $instance->cutoffdate; 1083 $duedate = $instance->duedate; 1084 $gradingduedate = $instance->gradingduedate; 1085 $mindate = null; 1086 $maxdate = null; 1087 1088 if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) { 1089 // This check is in here because due date events are currently 1090 // the only events that can be overridden, so we can save a DB 1091 // query if we don't bother checking other events. 1092 if ($this->is_override_calendar_event($event)) { 1093 // This is an override event so there is no valid timestart 1094 // range to set it to. 1095 return [false, false]; 1096 } 1097 1098 if ($submissionsfromdate) { 1099 $mindate = [ 1100 $submissionsfromdate, 1101 get_string('duedatevalidation', 'assign'), 1102 ]; 1103 } 1104 1105 if ($cutoffdate) { 1106 $maxdate = [ 1107 $cutoffdate, 1108 get_string('cutoffdatevalidation', 'assign'), 1109 ]; 1110 } 1111 1112 if ($gradingduedate) { 1113 // If we don't have a cutoff date or we've got a grading due date 1114 // that is earlier than the cutoff then we should use that as the 1115 // upper limit for the due date. 1116 if (!$cutoffdate || $gradingduedate < $cutoffdate) { 1117 $maxdate = [ 1118 $gradingduedate, 1119 get_string('gradingdueduedatevalidation', 'assign'), 1120 ]; 1121 } 1122 } 1123 } else if ($event->eventtype == ASSIGN_EVENT_TYPE_GRADINGDUE) { 1124 if ($duedate) { 1125 $mindate = [ 1126 $duedate, 1127 get_string('gradingdueduedatevalidation', 'assign'), 1128 ]; 1129 } else if ($submissionsfromdate) { 1130 $mindate = [ 1131 $submissionsfromdate, 1132 get_string('gradingduefromdatevalidation', 'assign'), 1133 ]; 1134 } 1135 } 1136 1137 return [$mindate, $maxdate]; 1138 } 1139 1140 /** 1141 * Actual implementation of the reset course functionality, delete all the 1142 * assignment submissions for course $data->courseid. 1143 * 1144 * @param stdClass $data the data submitted from the reset course. 1145 * @return array status array 1146 */ 1147 public function reset_userdata($data) { 1148 global $CFG, $DB; 1149 1150 $componentstr = get_string('modulenameplural', 'assign'); 1151 $status = array(); 1152 1153 $fs = get_file_storage(); 1154 if (!empty($data->reset_assign_submissions)) { 1155 // Delete files associated with this assignment. 1156 foreach ($this->submissionplugins as $plugin) { 1157 $fileareas = array(); 1158 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type(); 1159 $fileareas = $plugin->get_file_areas(); 1160 foreach ($fileareas as $filearea => $notused) { 1161 $fs->delete_area_files($this->context->id, $plugincomponent, $filearea); 1162 } 1163 1164 if (!$plugin->delete_instance()) { 1165 $status[] = array('component'=>$componentstr, 1166 'item'=>get_string('deleteallsubmissions', 'assign'), 1167 'error'=>$plugin->get_error()); 1168 } 1169 } 1170 1171 foreach ($this->feedbackplugins as $plugin) { 1172 $fileareas = array(); 1173 $plugincomponent = $plugin->get_subtype() . '_' . $plugin->get_type(); 1174 $fileareas = $plugin->get_file_areas(); 1175 foreach ($fileareas as $filearea => $notused) { 1176 $fs->delete_area_files($this->context->id, $plugincomponent, $filearea); 1177 } 1178 1179 if (!$plugin->delete_instance()) { 1180 $status[] = array('component'=>$componentstr, 1181 'item'=>get_string('deleteallsubmissions', 'assign'), 1182 'error'=>$plugin->get_error()); 1183 } 1184 } 1185 1186 $assignids = $DB->get_records('assign', array('course' => $data->courseid), '', 'id'); 1187 list($sql, $params) = $DB->get_in_or_equal(array_keys($assignids)); 1188 1189 $DB->delete_records_select('assign_submission', "assignment $sql", $params); 1190 $DB->delete_records_select('assign_user_flags', "assignment $sql", $params); 1191 1192 $status[] = array('component'=>$componentstr, 1193 'item'=>get_string('deleteallsubmissions', 'assign'), 1194 'error'=>false); 1195 1196 if (!empty($data->reset_gradebook_grades)) { 1197 $DB->delete_records_select('assign_grades', "assignment $sql", $params); 1198 // Remove all grades from gradebook. 1199 require_once($CFG->dirroot.'/mod/assign/lib.php'); 1200 assign_reset_gradebook($data->courseid); 1201 } 1202 1203 // Reset revealidentities for assign if blindmarking is enabled. 1204 if ($this->get_instance()->blindmarking) { 1205 $DB->set_field('assign', 'revealidentities', 0, array('id' => $this->get_instance()->id)); 1206 } 1207 } 1208 1209 $purgeoverrides = false; 1210 1211 // Remove user overrides. 1212 if (!empty($data->reset_assign_user_overrides)) { 1213 $DB->delete_records_select('assign_overrides', 1214 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid)); 1215 $status[] = array( 1216 'component' => $componentstr, 1217 'item' => get_string('useroverridesdeleted', 'assign'), 1218 'error' => false); 1219 $purgeoverrides = true; 1220 } 1221 // Remove group overrides. 1222 if (!empty($data->reset_assign_group_overrides)) { 1223 $DB->delete_records_select('assign_overrides', 1224 'assignid IN (SELECT id FROM {assign} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid)); 1225 $status[] = array( 1226 'component' => $componentstr, 1227 'item' => get_string('groupoverridesdeleted', 'assign'), 1228 'error' => false); 1229 $purgeoverrides = true; 1230 } 1231 1232 // Updating dates - shift may be negative too. 1233 if ($data->timeshift) { 1234 $DB->execute("UPDATE {assign_overrides} 1235 SET allowsubmissionsfromdate = allowsubmissionsfromdate + ? 1236 WHERE assignid = ? AND allowsubmissionsfromdate <> 0", 1237 array($data->timeshift, $this->get_instance()->id)); 1238 $DB->execute("UPDATE {assign_overrides} 1239 SET duedate = duedate + ? 1240 WHERE assignid = ? AND duedate <> 0", 1241 array($data->timeshift, $this->get_instance()->id)); 1242 $DB->execute("UPDATE {assign_overrides} 1243 SET cutoffdate = cutoffdate + ? 1244 WHERE assignid =? AND cutoffdate <> 0", 1245 array($data->timeshift, $this->get_instance()->id)); 1246 1247 $purgeoverrides = true; 1248 1249 // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. 1250 // See MDL-9367. 1251 shift_course_mod_dates('assign', 1252 array('duedate', 'allowsubmissionsfromdate', 'cutoffdate'), 1253 $data->timeshift, 1254 $data->courseid, $this->get_instance()->id); 1255 $status[] = array('component'=>$componentstr, 1256 'item'=>get_string('datechanged'), 1257 'error'=>false); 1258 } 1259 1260 if ($purgeoverrides) { 1261 cache::make('mod_assign', 'overrides')->purge(); 1262 } 1263 1264 return $status; 1265 } 1266 1267 /** 1268 * Update the settings for a single plugin. 1269 * 1270 * @param assign_plugin $plugin The plugin to update 1271 * @param stdClass $formdata The form data 1272 * @return bool false if an error occurs 1273 */ 1274 protected function update_plugin_instance(assign_plugin $plugin, stdClass $formdata) { 1275 if ($plugin->is_visible()) { 1276 $enabledname = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled'; 1277 if (!empty($formdata->$enabledname)) { 1278 $plugin->enable(); 1279 if (!$plugin->save_settings($formdata)) { 1280 print_error($plugin->get_error()); 1281 return false; 1282 } 1283 } else { 1284 $plugin->disable(); 1285 } 1286 } 1287 return true; 1288 } 1289 1290 /** 1291 * Update the gradebook information for this assignment. 1292 * 1293 * @param bool $reset If true, will reset all grades in the gradbook for this assignment 1294 * @param int $coursemoduleid This is required because it might not exist in the database yet 1295 * @return bool 1296 */ 1297 public function update_gradebook($reset, $coursemoduleid) { 1298 global $CFG; 1299 1300 require_once($CFG->dirroot.'/mod/assign/lib.php'); 1301 $assign = clone $this->get_instance(); 1302 $assign->cmidnumber = $coursemoduleid; 1303 1304 // Set assign gradebook feedback plugin status (enabled and visible). 1305 $assign->gradefeedbackenabled = $this->is_gradebook_feedback_enabled(); 1306 1307 $param = null; 1308 if ($reset) { 1309 $param = 'reset'; 1310 } 1311 1312 return assign_grade_item_update($assign, $param); 1313 } 1314 1315 /** 1316 * Get the marking table page size 1317 * 1318 * @return integer 1319 */ 1320 public function get_assign_perpage() { 1321 $perpage = (int) get_user_preferences('assign_perpage', 10); 1322 $adminconfig = $this->get_admin_config(); 1323 $maxperpage = -1; 1324 if (isset($adminconfig->maxperpage)) { 1325 $maxperpage = $adminconfig->maxperpage; 1326 } 1327 if (isset($maxperpage) && 1328 $maxperpage != -1 && 1329 ($perpage == -1 || $perpage > $maxperpage)) { 1330 $perpage = $maxperpage; 1331 } 1332 return $perpage; 1333 } 1334 1335 /** 1336 * Load and cache the admin config for this module. 1337 * 1338 * @return stdClass the plugin config 1339 */ 1340 public function get_admin_config() { 1341 if ($this->adminconfig) { 1342 return $this->adminconfig; 1343 } 1344 $this->adminconfig = get_config('assign'); 1345 return $this->adminconfig; 1346 } 1347 1348 /** 1349 * Update the calendar entries for this assignment. 1350 * 1351 * @param int $coursemoduleid - Required to pass this in because it might 1352 * not exist in the database yet. 1353 * @return bool 1354 */ 1355 public function update_calendar($coursemoduleid) { 1356 global $DB, $CFG; 1357 require_once($CFG->dirroot.'/calendar/lib.php'); 1358 1359 // Special case for add_instance as the coursemodule has not been set yet. 1360 $instance = $this->get_instance(); 1361 1362 // Start with creating the event. 1363 $event = new stdClass(); 1364 $event->modulename = 'assign'; 1365 $event->courseid = $instance->course; 1366 $event->groupid = 0; 1367 $event->userid = 0; 1368 $event->instance = $instance->id; 1369 $event->type = CALENDAR_EVENT_TYPE_ACTION; 1370 1371 // Convert the links to pluginfile. It is a bit hacky but at this stage the files 1372 // might not have been saved in the module area yet. 1373 $intro = $instance->intro; 1374 if ($draftid = file_get_submitted_draft_itemid('introeditor')) { 1375 $intro = file_rewrite_urls_to_pluginfile($intro, $draftid); 1376 } 1377 1378 // We need to remove the links to files as the calendar is not ready 1379 // to support module events with file areas. 1380 $intro = strip_pluginfile_content($intro); 1381 if ($this->show_intro()) { 1382 $event->description = array( 1383 'text' => $intro, 1384 'format' => $instance->introformat 1385 ); 1386 } else { 1387 $event->description = array( 1388 'text' => '', 1389 'format' => $instance->introformat 1390 ); 1391 } 1392 1393 $eventtype = ASSIGN_EVENT_TYPE_DUE; 1394 if ($instance->duedate) { 1395 $event->name = get_string('calendardue', 'assign', $instance->name); 1396 $event->eventtype = $eventtype; 1397 $event->timestart = $instance->duedate; 1398 $event->timesort = $instance->duedate; 1399 $select = "modulename = :modulename 1400 AND instance = :instance 1401 AND eventtype = :eventtype 1402 AND groupid = 0 1403 AND courseid <> 0"; 1404 $params = array('modulename' => 'assign', 'instance' => $instance->id, 'eventtype' => $eventtype); 1405 $event->id = $DB->get_field_select('event', 'id', $select, $params); 1406 1407 // Now process the event. 1408 if ($event->id) { 1409 $calendarevent = calendar_event::load($event->id); 1410 $calendarevent->update($event, false); 1411 } else { 1412 calendar_event::create($event, false); 1413 } 1414 } else { 1415 $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id, 1416 'eventtype' => $eventtype)); 1417 } 1418 1419 $eventtype = ASSIGN_EVENT_TYPE_GRADINGDUE; 1420 if ($instance->gradingduedate) { 1421 $event->name = get_string('calendargradingdue', 'assign', $instance->name); 1422 $event->eventtype = $eventtype; 1423 $event->timestart = $instance->gradingduedate; 1424 $event->timesort = $instance->gradingduedate; 1425 $event->id = $DB->get_field('event', 'id', array('modulename' => 'assign', 1426 'instance' => $instance->id, 'eventtype' => $event->eventtype)); 1427 1428 // Now process the event. 1429 if ($event->id) { 1430 $calendarevent = calendar_event::load($event->id); 1431 $calendarevent->update($event, false); 1432 } else { 1433 calendar_event::create($event, false); 1434 } 1435 } else { 1436 $DB->delete_records('event', array('modulename' => 'assign', 'instance' => $instance->id, 1437 'eventtype' => $eventtype)); 1438 } 1439 1440 return true; 1441 } 1442 1443 /** 1444 * Update this instance in the database. 1445 * 1446 * @param stdClass $formdata - the data submitted from the form 1447 * @return bool false if an error occurs 1448 */ 1449 public function update_instance($formdata) { 1450 global $DB; 1451 $adminconfig = $this->get_admin_config(); 1452 1453 $update = new stdClass(); 1454 $update->id = $formdata->instance; 1455 $update->name = $formdata->name; 1456 $update->timemodified = time(); 1457 $update->course = $formdata->course; 1458 $update->intro = $formdata->intro; 1459 $update->introformat = $formdata->introformat; 1460 $update->alwaysshowdescription = !empty($formdata->alwaysshowdescription); 1461 $update->submissiondrafts = $formdata->submissiondrafts; 1462 $update->requiresubmissionstatement = $formdata->requiresubmissionstatement; 1463 $update->sendnotifications = $formdata->sendnotifications; 1464 $update->sendlatenotifications = $formdata->sendlatenotifications; 1465 $update->sendstudentnotifications = $adminconfig->sendstudentnotifications; 1466 if (isset($formdata->sendstudentnotifications)) { 1467 $update->sendstudentnotifications = $formdata->sendstudentnotifications; 1468 } 1469 $update->duedate = $formdata->duedate; 1470 $update->cutoffdate = $formdata->cutoffdate; 1471 $update->gradingduedate = $formdata->gradingduedate; 1472 $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate; 1473 $update->grade = $formdata->grade; 1474 if (!empty($formdata->completionunlocked)) { 1475 $update->completionsubmit = !empty($formdata->completionsubmit); 1476 } 1477 $update->teamsubmission = $formdata->teamsubmission; 1478 $update->requireallteammemberssubmit = $formdata->requireallteammemberssubmit; 1479 if (isset($formdata->teamsubmissiongroupingid)) { 1480 $update->teamsubmissiongroupingid = $formdata->teamsubmissiongroupingid; 1481 } 1482 if (isset($formdata->hidegrader)) { 1483 $update->hidegrader = $formdata->hidegrader; 1484 } 1485 $update->blindmarking = $formdata->blindmarking; 1486 $update->attemptreopenmethod = ASSIGN_ATTEMPT_REOPEN_METHOD_NONE; 1487 if (!empty($formdata->attemptreopenmethod)) { 1488 $update->attemptreopenmethod = $formdata->attemptreopenmethod; 1489 } 1490 if (!empty($formdata->maxattempts)) { 1491 $update->maxattempts = $formdata->maxattempts; 1492 } 1493 if (isset($formdata->preventsubmissionnotingroup)) { 1494 $update->preventsubmissionnotingroup = $formdata->preventsubmissionnotingroup; 1495 } 1496 $update->markingworkflow = $formdata->markingworkflow; 1497 $update->markingallocation = $formdata->markingallocation; 1498 if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled. 1499 $update->markingallocation = 0; 1500 } 1501 1502 $result = $DB->update_record('assign', $update); 1503 $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST); 1504 1505 $this->save_intro_draft_files($formdata); 1506 1507 // Load the assignment so the plugins have access to it. 1508 1509 // Call save_settings hook for submission plugins. 1510 foreach ($this->submissionplugins as $plugin) { 1511 if (!$this->update_plugin_instance($plugin, $formdata)) { 1512 print_error($plugin->get_error()); 1513 return false; 1514 } 1515 } 1516 foreach ($this->feedbackplugins as $plugin) { 1517 if (!$this->update_plugin_instance($plugin, $formdata)) { 1518 print_error($plugin->get_error()); 1519 return false; 1520 } 1521 } 1522 1523 $this->update_calendar($this->get_course_module()->id); 1524 $completionexpected = (!empty($formdata->completionexpected)) ? $formdata->completionexpected : null; 1525 \core_completion\api::update_completion_date_event($this->get_course_module()->id, 'assign', $this->instance, 1526 $completionexpected); 1527 $this->update_gradebook(false, $this->get_course_module()->id); 1528 1529 $update = new stdClass(); 1530 $update->id = $this->get_instance()->id; 1531 $update->nosubmissions = (!$this->is_any_submission_plugin_enabled()) ? 1: 0; 1532 $DB->update_record('assign', $update); 1533 1534 return $result; 1535 } 1536 1537 /** 1538 * Save the attachments in the draft areas. 1539 * 1540 * @param stdClass $formdata 1541 */ 1542 protected function save_intro_draft_files($formdata) { 1543 if (isset($formdata->introattachments)) { 1544 file_save_draft_area_files($formdata->introattachments, $this->get_context()->id, 1545 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0); 1546 } 1547 } 1548 1549 /** 1550 * Add elements in grading plugin form. 1551 * 1552 * @param mixed $grade stdClass|null 1553 * @param MoodleQuickForm $mform 1554 * @param stdClass $data 1555 * @param int $userid - The userid we are grading 1556 * @return void 1557 */ 1558 protected function add_plugin_grade_elements($grade, MoodleQuickForm $mform, stdClass $data, $userid) { 1559 foreach ($this->feedbackplugins as $plugin) { 1560 if ($plugin->is_enabled() && $plugin->is_visible()) { 1561 $plugin->get_form_elements_for_user($grade, $mform, $data, $userid); 1562 } 1563 } 1564 } 1565 1566 1567 1568 /** 1569 * Add one plugins settings to edit plugin form. 1570 * 1571 * @param assign_plugin $plugin The plugin to add the settings from 1572 * @param MoodleQuickForm $mform The form to add the configuration settings to. 1573 * This form is modified directly (not returned). 1574 * @param array $pluginsenabled A list of form elements to be added to a group. 1575 * The new element is added to this array by this function. 1576 * @return void 1577 */ 1578 protected function add_plugin_settings(assign_plugin $plugin, MoodleQuickForm $mform, & $pluginsenabled) { 1579 global $CFG; 1580 if ($plugin->is_visible() && !$plugin->is_configurable() && $plugin->is_enabled()) { 1581 $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled'; 1582 $pluginsenabled[] = $mform->createElement('hidden', $name, 1); 1583 $mform->setType($name, PARAM_BOOL); 1584 $plugin->get_settings($mform); 1585 } else if ($plugin->is_visible() && $plugin->is_configurable()) { 1586 $name = $plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled'; 1587 $label = $plugin->get_name(); 1588 $pluginsenabled[] = $mform->createElement('checkbox', $name, '', $label); 1589 $helpicon = $this->get_renderer()->help_icon('enabled', $plugin->get_subtype() . '_' . $plugin->get_type()); 1590 $pluginsenabled[] = $mform->createElement('static', '', '', $helpicon); 1591 1592 $default = get_config($plugin->get_subtype() . '_' . $plugin->get_type(), 'default'); 1593 if ($plugin->get_config('enabled') !== false) { 1594 $default = $plugin->is_enabled(); 1595 } 1596 $mform->setDefault($plugin->get_subtype() . '_' . $plugin->get_type() . '_enabled', $default); 1597 1598 $plugin->get_settings($mform); 1599 1600 } 1601 } 1602 1603 /** 1604 * Add settings to edit plugin form. 1605 * 1606 * @param MoodleQuickForm $mform The form to add the configuration settings to. 1607 * This form is modified directly (not returned). 1608 * @return void 1609 */ 1610 public function add_all_plugin_settings(MoodleQuickForm $mform) { 1611 $mform->addElement('header', 'submissiontypes', get_string('submissiontypes', 'assign')); 1612 1613 $submissionpluginsenabled = array(); 1614 $group = $mform->addGroup(array(), 'submissionplugins', get_string('submissiontypes', 'assign'), array(' '), false); 1615 foreach ($this->submissionplugins as $plugin) { 1616 $this->add_plugin_settings($plugin, $mform, $submissionpluginsenabled); 1617 } 1618 $group->setElements($submissionpluginsenabled); 1619 1620 $mform->addElement('header', 'feedbacktypes', get_string('feedbacktypes', 'assign')); 1621 $feedbackpluginsenabled = array(); 1622 $group = $mform->addGroup(array(), 'feedbackplugins', get_string('feedbacktypes', 'assign'), array(' '), false); 1623 foreach ($this->feedbackplugins as $plugin) { 1624 $this->add_plugin_settings($plugin, $mform, $feedbackpluginsenabled); 1625 } 1626 $group->setElements($feedbackpluginsenabled); 1627 $mform->setExpanded('submissiontypes'); 1628 } 1629 1630 /** 1631 * Allow each plugin an opportunity to update the defaultvalues 1632 * passed in to the settings form (needed to set up draft areas for 1633 * editor and filemanager elements) 1634 * 1635 * @param array $defaultvalues 1636 */ 1637 public function plugin_data_preprocessing(&$defaultvalues) { 1638 foreach ($this->submissionplugins as $plugin) { 1639 if ($plugin->is_visible()) { 1640 $plugin->data_preprocessing($defaultvalues); 1641 } 1642 } 1643 foreach ($this->feedbackplugins as $plugin) { 1644 if ($plugin->is_visible()) { 1645 $plugin->data_preprocessing($defaultvalues); 1646 } 1647 } 1648 } 1649 1650 /** 1651 * Get the name of the current module. 1652 * 1653 * @return string the module name (Assignment) 1654 */ 1655 protected function get_module_name() { 1656 if (isset(self::$modulename)) { 1657 return self::$modulename; 1658 } 1659 self::$modulename = get_string('modulename', 'assign'); 1660 return self::$modulename; 1661 } 1662 1663 /** 1664 * Get the plural name of the current module. 1665 * 1666 * @return string the module name plural (Assignments) 1667 */ 1668 protected function get_module_name_plural() { 1669 if (isset(self::$modulenameplural)) { 1670 return self::$modulenameplural; 1671 } 1672 self::$modulenameplural = get_string('modulenameplural', 'assign'); 1673 return self::$modulenameplural; 1674 } 1675 1676 /** 1677 * Has this assignment been constructed from an instance? 1678 * 1679 * @return bool 1680 */ 1681 public function has_instance() { 1682 return $this->instance || $this->get_course_module(); 1683 } 1684 1685 /** 1686 * Get the settings for the current instance of this assignment. 1687 * 1688 * @return stdClass The settings 1689 * @throws dml_exception 1690 */ 1691 public function get_default_instance() { 1692 global $DB; 1693 if (!$this->instance && $this->get_course_module()) { 1694 $params = array('id' => $this->get_course_module()->instance); 1695 $this->instance = $DB->get_record('assign', $params, '*', MUST_EXIST); 1696 1697 $this->userinstances = []; 1698 } 1699 return $this->instance; 1700 } 1701 1702 /** 1703 * Get the settings for the current instance of this assignment 1704 * @param int|null $userid the id of the user to load the assign instance for. 1705 * @return stdClass The settings 1706 */ 1707 public function get_instance(int $userid = null) : stdClass { 1708 global $USER; 1709 $userid = $userid ?? $USER->id; 1710 1711 $this->instance = $this->get_default_instance(); 1712 1713 // If we have the user instance already, just return it. 1714 if (isset($this->userinstances[$userid])) { 1715 return $this->userinstances[$userid]; 1716 } 1717 1718 // Calculate properties which vary per user. 1719 $this->userinstances[$userid] = $this->calculate_properties($this->instance, $userid); 1720 return $this->userinstances[$userid]; 1721 } 1722 1723 /** 1724 * Calculates and updates various properties based on the specified user. 1725 * 1726 * @param stdClass $record the raw assign record. 1727 * @param int $userid the id of the user to calculate the properties for. 1728 * @return stdClass a new record having calculated properties. 1729 */ 1730 private function calculate_properties(\stdClass $record, int $userid) : \stdClass { 1731 $record = clone ($record); 1732 1733 // Relative dates. 1734 if (!empty($record->duedate)) { 1735 $course = $this->get_course(); 1736 $usercoursedates = course_get_course_dates_for_user_id($course, $userid); 1737 if ($usercoursedates['start']) { 1738 $userprops = ['duedate' => $record->duedate + $usercoursedates['startoffset']]; 1739 $record = (object) array_merge((array) $record, (array) $userprops); 1740 } 1741 } 1742 return $record; 1743 } 1744 1745 /** 1746 * Get the primary grade item for this assign instance. 1747 * 1748 * @return grade_item The grade_item record 1749 */ 1750 public function get_grade_item() { 1751 if ($this->gradeitem) { 1752 return $this->gradeitem; 1753 } 1754 $instance = $this->get_instance(); 1755 $params = array('itemtype' => 'mod', 1756 'itemmodule' => 'assign', 1757 'iteminstance' => $instance->id, 1758 'courseid' => $instance->course, 1759 'itemnumber' => 0); 1760 $this->gradeitem = grade_item::fetch($params); 1761 if (!$this->gradeitem) { 1762 throw new coding_exception('Improper use of the assignment class. ' . 1763 'Cannot load the grade item.'); 1764 } 1765 return $this->gradeitem; 1766 } 1767 1768 /** 1769 * Get the context of the current course. 1770 * 1771 * @return mixed context|null The course context 1772 */ 1773 public function get_course_context() { 1774 if (!$this->context && !$this->course) { 1775 throw new coding_exception('Improper use of the assignment class. ' . 1776 'Cannot load the course context.'); 1777 } 1778 if ($this->context) { 1779 return $this->context->get_course_context(); 1780 } else { 1781 return context_course::instance($this->course->id); 1782 } 1783 } 1784 1785 1786 /** 1787 * Get the current course module. 1788 * 1789 * @return cm_info|null The course module or null if not known 1790 */ 1791 public function get_course_module() { 1792 if ($this->coursemodule) { 1793 return $this->coursemodule; 1794 } 1795 if (!$this->context) { 1796 return null; 1797 } 1798 1799 if ($this->context->contextlevel == CONTEXT_MODULE) { 1800 $modinfo = get_fast_modinfo($this->get_course()); 1801 $this->coursemodule = $modinfo->get_cm($this->context->instanceid); 1802 return $this->coursemodule; 1803 } 1804 return null; 1805 } 1806 1807 /** 1808 * Get context module. 1809 * 1810 * @return context 1811 */ 1812 public function get_context() { 1813 return $this->context; 1814 } 1815 1816 /** 1817 * Get the current course. 1818 * 1819 * @return mixed stdClass|null The course 1820 */ 1821 public function get_course() { 1822 global $DB; 1823 1824 if ($this->course && is_object($this->course)) { 1825 return $this->course; 1826 } 1827 1828 if (!$this->context) { 1829 return null; 1830 } 1831 $params = array('id' => $this->get_course_context()->instanceid); 1832 $this->course = $DB->get_record('course', $params, '*', MUST_EXIST); 1833 1834 return $this->course; 1835 } 1836 1837 /** 1838 * Count the number of intro attachments. 1839 * 1840 * @return int 1841 */ 1842 protected function count_attachments() { 1843 1844 $fs = get_file_storage(); 1845 $files = $fs->get_area_files($this->get_context()->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 1846 0, 'id', false); 1847 1848 return count($files); 1849 } 1850 1851 /** 1852 * Are there any intro attachments to display? 1853 * 1854 * @return boolean 1855 */ 1856 protected function has_visible_attachments() { 1857 return ($this->count_attachments() > 0); 1858 } 1859 1860 /** 1861 * Return a grade in user-friendly form, whether it's a scale or not. 1862 * 1863 * @param mixed $grade int|null 1864 * @param boolean $editing Are we allowing changes to this grade? 1865 * @param int $userid The user id the grade belongs to 1866 * @param int $modified Timestamp from when the grade was last modified 1867 * @return string User-friendly representation of grade 1868 */ 1869 public function display_grade($grade, $editing, $userid=0, $modified=0) { 1870 global $DB; 1871 1872 static $scalegrades = array(); 1873 1874 $o = ''; 1875 1876 if ($this->get_instance()->grade >= 0) { 1877 // Normal number. 1878 if ($editing && $this->get_instance()->grade > 0) { 1879 if ($grade < 0) { 1880 $displaygrade = ''; 1881 } else { 1882 $displaygrade = format_float($grade, $this->get_grade_item()->get_decimals()); 1883 } 1884 $o .= '<label class="accesshide" for="quickgrade_' . $userid . '">' . 1885 get_string('usergrade', 'assign') . 1886 '</label>'; 1887 $o .= '<input type="text" 1888 id="quickgrade_' . $userid . '" 1889 name="quickgrade_' . $userid . '" 1890 value="' . $displaygrade . '" 1891 size="6" 1892 maxlength="10" 1893 class="quickgrade"/>'; 1894 $o .= ' / ' . format_float($this->get_instance()->grade, $this->get_grade_item()->get_decimals()); 1895 return $o; 1896 } else { 1897 if ($grade == -1 || $grade === null) { 1898 $o .= '-'; 1899 } else { 1900 $item = $this->get_grade_item(); 1901 $o .= grade_format_gradevalue($grade, $item); 1902 if ($item->get_displaytype() == GRADE_DISPLAY_TYPE_REAL) { 1903 // If displaying the raw grade, also display the total value. 1904 $o .= ' / ' . format_float($this->get_instance()->grade, $item->get_decimals()); 1905 } 1906 } 1907 return $o; 1908 } 1909 1910 } else { 1911 // Scale. 1912 if (empty($this->cache['scale'])) { 1913 if ($scale = $DB->get_record('scale', array('id'=>-($this->get_instance()->grade)))) { 1914 $this->cache['scale'] = make_menu_from_list($scale->scale); 1915 } else { 1916 $o .= '-'; 1917 return $o; 1918 } 1919 } 1920 if ($editing) { 1921 $o .= '<label class="accesshide" 1922 for="quickgrade_' . $userid . '">' . 1923 get_string('usergrade', 'assign') . 1924 '</label>'; 1925 $o .= '<select name="quickgrade_' . $userid . '" class="quickgrade">'; 1926 $o .= '<option value="-1">' . get_string('nograde') . '</option>'; 1927 foreach ($this->cache['scale'] as $optionid => $option) { 1928 $selected = ''; 1929 if ($grade == $optionid) { 1930 $selected = 'selected="selected"'; 1931 } 1932 $o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>'; 1933 } 1934 $o .= '</select>'; 1935 return $o; 1936 } else { 1937 $scaleid = (int)$grade; 1938 if (isset($this->cache['scale'][$scaleid])) { 1939 $o .= $this->cache['scale'][$scaleid]; 1940 return $o; 1941 } 1942 $o .= '-'; 1943 return $o; 1944 } 1945 } 1946 } 1947 1948 /** 1949 * Get the submission status/grading status for all submissions in this assignment for the 1950 * given paticipants. 1951 * 1952 * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension). 1953 * If this is a group assignment, group info is also returned. 1954 * 1955 * @param array $participants an associative array where the key is the participant id and 1956 * the value is the participant record. 1957 * @return array an associative array where the key is the participant id and the value is 1958 * the participant record. 1959 */ 1960 private function get_submission_info_for_participants($participants) { 1961 global $DB; 1962 1963 if (empty($participants)) { 1964 return $participants; 1965 } 1966 1967 list($insql, $params) = $DB->get_in_or_equal(array_keys($participants), SQL_PARAMS_NAMED); 1968 1969 $assignid = $this->get_instance()->id; 1970 $params['assignmentid1'] = $assignid; 1971 $params['assignmentid2'] = $assignid; 1972 $params['assignmentid3'] = $assignid; 1973 1974 $fields = 'SELECT u.id, s.status, s.timemodified AS stime, g.timemodified AS gtime, g.grade, uf.extensionduedate'; 1975 $from = ' FROM {user} u 1976 LEFT JOIN {assign_submission} s 1977 ON u.id = s.userid 1978 AND s.assignment = :assignmentid1 1979 AND s.latest = 1 1980 LEFT JOIN {assign_grades} g 1981 ON u.id = g.userid 1982 AND g.assignment = :assignmentid2 1983 AND g.attemptnumber = s.attemptnumber 1984 LEFT JOIN {assign_user_flags} uf 1985 ON u.id = uf.userid 1986 AND uf.assignment = :assignmentid3 1987 '; 1988 $where = ' WHERE u.id ' . $insql; 1989 1990 if (!empty($this->get_instance()->blindmarking)) { 1991 $from .= 'LEFT JOIN {assign_user_mapping} um 1992 ON u.id = um.userid 1993 AND um.assignment = :assignmentid4 '; 1994 $params['assignmentid4'] = $assignid; 1995 $fields .= ', um.id as recordid '; 1996 } 1997 1998 $sql = "$fields $from $where"; 1999 2000 $records = $DB->get_records_sql($sql, $params); 2001 2002 if ($this->get_instance()->teamsubmission) { 2003 // Get all groups. 2004 $allgroups = groups_get_all_groups($this->get_course()->id, 2005 array_keys($participants), 2006 $this->get_instance()->teamsubmissiongroupingid, 2007 'DISTINCT g.id, g.name'); 2008 2009 } 2010 foreach ($participants as $userid => $participant) { 2011 $participants[$userid]->fullname = $this->fullname($participant); 2012 $participants[$userid]->submitted = false; 2013 $participants[$userid]->requiregrading = false; 2014 $participants[$userid]->grantedextension = false; 2015 } 2016 2017 foreach ($records as $userid => $submissioninfo) { 2018 // These filters are 100% the same as the ones in the grading table SQL. 2019 $submitted = false; 2020 $requiregrading = false; 2021 $grantedextension = false; 2022 2023 if (!empty($submissioninfo->stime) && $submissioninfo->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) { 2024 $submitted = true; 2025 } 2026 2027 if ($submitted && ($submissioninfo->stime >= $submissioninfo->gtime || 2028 empty($submissioninfo->gtime) || 2029 $submissioninfo->grade === null)) { 2030 $requiregrading = true; 2031 } 2032 2033 if (!empty($submissioninfo->extensionduedate)) { 2034 $grantedextension = true; 2035 } 2036 2037 $participants[$userid]->submitted = $submitted; 2038 $participants[$userid]->requiregrading = $requiregrading; 2039 $participants[$userid]->grantedextension = $grantedextension; 2040 if ($this->get_instance()->teamsubmission) { 2041 $group = $this->get_submission_group($userid); 2042 if ($group) { 2043 $participants[$userid]->groupid = $group->id; 2044 $participants[$userid]->groupname = $group->name; 2045 } 2046 } 2047 } 2048 return $participants; 2049 } 2050 2051 /** 2052 * Get the submission status/grading status for all submissions in this assignment. 2053 * These statuses match the available filters (requiregrading, submitted, notsubmitted, grantedextension). 2054 * If this is a group assignment, group info is also returned. 2055 * 2056 * @param int $currentgroup 2057 * @param boolean $tablesort Apply current user table sorting preferences. 2058 * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'grantedextension', 2059 * 'groupid', 'groupname' 2060 */ 2061 public function list_participants_with_filter_status_and_group($currentgroup, $tablesort = false) { 2062 $participants = $this->list_participants($currentgroup, false, $tablesort); 2063 2064 if (empty($participants)) { 2065 return $participants; 2066 } else { 2067 return $this->get_submission_info_for_participants($participants); 2068 } 2069 } 2070 2071 /** 2072 * Return a valid order by segment for list_participants that matches 2073 * the sorting of the current grading table. Not every field is supported, 2074 * we are only concerned with a list of users so we can't search on anything 2075 * that is not part of the user information (like grading statud or last modified stuff). 2076 * 2077 * @return string Order by clause for list_participants 2078 */ 2079 private function get_grading_sort_sql() { 2080 $usersort = flexible_table::get_sort_for_table('mod_assign_grading'); 2081 // TODO Does not support custom user profile fields (MDL-70456). 2082 $userfieldsapi = \core_user\fields::for_identity($this->context, false)->with_userpic(); 2083 $userfields = $userfieldsapi->get_required_fields(); 2084 $orderfields = explode(',', $usersort); 2085 $validlist = []; 2086 2087 foreach ($orderfields as $orderfield) { 2088 $orderfield = trim($orderfield); 2089 foreach ($userfields as $field) { 2090 $parts = explode(' ', $orderfield); 2091 if ($parts[0] == $field) { 2092 // Prepend the user table prefix and count this as a valid order field. 2093 array_push($validlist, 'u.' . $orderfield); 2094 } 2095 } 2096 } 2097 // Produce a final list. 2098 $result = implode(',', $validlist); 2099 if (empty($result)) { 2100 // Fall back ordering when none has been set. 2101 $result = 'u.lastname, u.firstname, u.id'; 2102 } 2103 2104 return $result; 2105 } 2106 2107 /** 2108 * Returns array with sql code and parameters returning all ids of users who have submitted an assignment. 2109 * 2110 * @param int $group The group that the query is for. 2111 * @return array list($sql, $params) 2112 */ 2113 protected function get_submitted_sql($group = 0) { 2114 // We need to guarentee unique table names. 2115 static $i = 0; 2116 $i++; 2117 $prefix = 'sa' . $i . '_'; 2118 $params = [ 2119 "{$prefix}assignment" => (int) $this->get_instance()->id, 2120 "{$prefix}status" => ASSIGN_SUBMISSION_STATUS_NEW, 2121 ]; 2122 $capjoin = get_enrolled_with_capabilities_join($this->context, $prefix, '', $group, $this->show_only_active_users()); 2123 $params += $capjoin->params; 2124 $sql = "SELECT {$prefix}s.userid 2125 FROM {assign_submission} {$prefix}s 2126 JOIN {user} {$prefix}u ON {$prefix}u.id = {$prefix}s.userid 2127 $capjoin->joins 2128 WHERE {$prefix}s.assignment = :{$prefix}assignment 2129 AND {$prefix}s.status <> :{$prefix}status 2130 AND $capjoin->wheres"; 2131 return array($sql, $params); 2132 } 2133 2134 /** 2135 * Load a list of users enrolled in the current course with the specified permission and group. 2136 * 0 for no group. 2137 * Apply any current sort filters from the grading table. 2138 * 2139 * @param int $currentgroup 2140 * @param bool $idsonly 2141 * @param bool $tablesort 2142 * @return array List of user records 2143 */ 2144 public function list_participants($currentgroup, $idsonly, $tablesort = false) { 2145 global $DB, $USER; 2146 2147 // Get the last known sort order for the grading table. 2148 2149 if (empty($currentgroup)) { 2150 $currentgroup = 0; 2151 } 2152 2153 $key = $this->context->id . '-' . $currentgroup . '-' . $this->show_only_active_users(); 2154 if (!isset($this->participants[$key])) { 2155 list($esql, $params) = get_enrolled_sql($this->context, 'mod/assign:submit', $currentgroup, 2156 $this->show_only_active_users()); 2157 list($ssql, $sparams) = $this->get_submitted_sql($currentgroup); 2158 $params += $sparams; 2159 2160 $fields = 'u.*'; 2161 $orderby = 'u.lastname, u.firstname, u.id'; 2162 2163 $additionaljoins = ''; 2164 $additionalfilters = ''; 2165 $instance = $this->get_instance(); 2166 if (!empty($instance->blindmarking)) { 2167 $additionaljoins .= " LEFT JOIN {assign_user_mapping} um 2168 ON u.id = um.userid 2169 AND um.assignment = :assignmentid1 2170 LEFT JOIN {assign_submission} s 2171 ON u.id = s.userid 2172 AND s.assignment = :assignmentid2 2173 AND s.latest = 1 2174 "; 2175 $params['assignmentid1'] = (int) $instance->id; 2176 $params['assignmentid2'] = (int) $instance->id; 2177 $fields .= ', um.id as recordid '; 2178 2179 // Sort by submission time first, then by um.id to sort reliably by the blind marking id. 2180 // Note, different DBs have different ordering of NULL values. 2181 // Therefore we coalesce the current time into the timecreated field, and the max possible integer into 2182 // the ID field. 2183 if (empty($tablesort)) { 2184 $orderby = "COALESCE(s.timecreated, " . time() . ") ASC, COALESCE(s.id, " . PHP_INT_MAX . ") ASC, um.id ASC"; 2185 } 2186 } 2187 2188 if ($instance->markingworkflow && 2189 $instance->markingallocation && 2190 !has_capability('mod/assign:manageallocations', $this->get_context()) && 2191 has_capability('mod/assign:grade', $this->get_context())) { 2192 2193 $additionaljoins .= ' LEFT JOIN {assign_user_flags} uf 2194 ON u.id = uf.userid 2195 AND uf.assignment = :assignmentid3'; 2196 2197 $params['assignmentid3'] = (int) $instance->id; 2198 2199 $additionalfilters .= ' AND uf.allocatedmarker = :markerid'; 2200 $params['markerid'] = $USER->id; 2201 } 2202 2203 $sql = "SELECT $fields 2204 FROM {user} u 2205 JOIN ($esql UNION $ssql) je ON je.id = u.id 2206 $additionaljoins 2207 WHERE u.deleted = 0 2208 $additionalfilters 2209 ORDER BY $orderby"; 2210 2211 $users = $DB->get_records_sql($sql, $params); 2212 2213 $cm = $this->get_course_module(); 2214 $info = new \core_availability\info_module($cm); 2215 $users = $info->filter_user_list($users); 2216 2217 $this->participants[$key] = $users; 2218 } 2219 2220 if ($tablesort) { 2221 // Resort the user list according to the grading table sort and filter settings. 2222 $sortedfiltereduserids = $this->get_grading_userid_list(true, ''); 2223 $sortedfilteredusers = []; 2224 foreach ($sortedfiltereduserids as $nextid) { 2225 $nextid = intval($nextid); 2226 if (isset($this->participants[$key][$nextid])) { 2227 $sortedfilteredusers[$nextid] = $this->participants[$key][$nextid]; 2228 } 2229 } 2230 $this->participants[$key] = $sortedfilteredusers; 2231 } 2232 2233 if ($idsonly) { 2234 $idslist = array(); 2235 foreach ($this->participants[$key] as $id => $user) { 2236 $idslist[$id] = new stdClass(); 2237 $idslist[$id]->id = $id; 2238 } 2239 return $idslist; 2240 } 2241 return $this->participants[$key]; 2242 } 2243 2244 /** 2245 * Load a user if they are enrolled in the current course. Populated with submission 2246 * status for this assignment. 2247 * 2248 * @param int $userid 2249 * @return null|stdClass user record 2250 */ 2251 public function get_participant($userid) { 2252 global $DB, $USER; 2253 2254 if ($userid == $USER->id) { 2255 $participant = clone ($USER); 2256 } else { 2257 $participant = $DB->get_record('user', array('id' => $userid)); 2258 } 2259 if (!$participant) { 2260 return null; 2261 } 2262 2263 if (!is_enrolled($this->context, $participant, '', $this->show_only_active_users())) { 2264 return null; 2265 } 2266 2267 $result = $this->get_submission_info_for_participants(array($participant->id => $participant)); 2268 2269 $submissioninfo = $result[$participant->id]; 2270 if (!$submissioninfo->submitted && !has_capability('mod/assign:submit', $this->context, $userid)) { 2271 return null; 2272 } 2273 2274 return $submissioninfo; 2275 } 2276 2277 /** 2278 * Load a count of valid teams for this assignment. 2279 * 2280 * @param int $activitygroup Activity active group 2281 * @return int number of valid teams 2282 */ 2283 public function count_teams($activitygroup = 0) { 2284 2285 $count = 0; 2286 2287 $participants = $this->list_participants($activitygroup, true); 2288 2289 // If a team submission grouping id is provided all good as all returned groups 2290 // are the submission teams, but if no team submission grouping was specified 2291 // $groups will contain all participants groups. 2292 if ($this->get_instance()->teamsubmissiongroupingid) { 2293 2294 // We restrict the users to the selected group ones. 2295 $groups = groups_get_all_groups($this->get_course()->id, 2296 array_keys($participants), 2297 $this->get_instance()->teamsubmissiongroupingid, 2298 'DISTINCT g.id, g.name'); 2299 2300 $count = count($groups); 2301 2302 // When a specific group is selected we don't count the default group users. 2303 if ($activitygroup == 0) { 2304 if (empty($this->get_instance()->preventsubmissionnotingroup)) { 2305 // See if there are any users in the default group. 2306 $defaultusers = $this->get_submission_group_members(0, true); 2307 if (count($defaultusers) > 0) { 2308 $count += 1; 2309 } 2310 } 2311 } else if ($activitygroup != 0 && empty($groups)) { 2312 // Set count to 1 if $groups returns empty. 2313 // It means the group is not part of $this->get_instance()->teamsubmissiongroupingid. 2314 $count = 1; 2315 } 2316 } else { 2317 // It is faster to loop around participants if no grouping was specified. 2318 $groups = array(); 2319 foreach ($participants as $participant) { 2320 if ($group = $this->get_submission_group($participant->id)) { 2321 $groups[$group->id] = true; 2322 } else if (empty($this->get_instance()->preventsubmissionnotingroup)) { 2323 $groups[0] = true; 2324 } 2325 } 2326 2327 $count = count($groups); 2328 } 2329 2330 return $count; 2331 } 2332 2333 /** 2334 * Load a count of active users enrolled in the current course with the specified permission and group. 2335 * 0 for no group. 2336 * 2337 * @param int $currentgroup 2338 * @return int number of matching users 2339 */ 2340 public function count_participants($currentgroup) { 2341 return count($this->list_participants($currentgroup, true)); 2342 } 2343 2344 /** 2345 * Load a count of active users submissions in the current module that require grading 2346 * This means the submission modification time is more recent than the 2347 * grading modification time and the status is SUBMITTED. 2348 * 2349 * @param mixed $currentgroup int|null the group for counting (if null the function will determine it) 2350 * @return int number of matching submissions 2351 */ 2352 public function count_submissions_need_grading($currentgroup = null) { 2353 global $DB; 2354 2355 if ($this->get_instance()->teamsubmission) { 2356 // This does not make sense for group assignment because the submission is shared. 2357 return 0; 2358 } 2359 2360 if ($currentgroup === null) { 2361 $currentgroup = groups_get_activity_group($this->get_course_module(), true); 2362 } 2363 list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true); 2364 2365 $params['assignid'] = $this->get_instance()->id; 2366 $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED; 2367 $sqlscalegrade = $this->get_instance()->grade < 0 ? ' OR g.grade = -1' : ''; 2368 2369 $sql = 'SELECT COUNT(s.userid) 2370 FROM {assign_submission} s 2371 LEFT JOIN {assign_grades} g ON 2372 s.assignment = g.assignment AND 2373 s.userid = g.userid AND 2374 g.attemptnumber = s.attemptnumber 2375 JOIN(' . $esql . ') e ON e.id = s.userid 2376 WHERE 2377 s.latest = 1 AND 2378 s.assignment = :assignid AND 2379 s.timemodified IS NOT NULL AND 2380 s.status = :submitted AND 2381 (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL ' 2382 . $sqlscalegrade . ')'; 2383 2384 return $DB->count_records_sql($sql, $params); 2385 } 2386 2387 /** 2388 * Load a count of grades. 2389 * 2390 * @return int number of grades 2391 */ 2392 public function count_grades() { 2393 global $DB; 2394 2395 if (!$this->has_instance()) { 2396 return 0; 2397 } 2398 2399 $currentgroup = groups_get_activity_group($this->get_course_module(), true); 2400 list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true); 2401 2402 $params['assignid'] = $this->get_instance()->id; 2403 2404 $sql = 'SELECT COUNT(g.userid) 2405 FROM {assign_grades} g 2406 JOIN(' . $esql . ') e ON e.id = g.userid 2407 WHERE g.assignment = :assignid'; 2408 2409 return $DB->count_records_sql($sql, $params); 2410 } 2411 2412 /** 2413 * Load a count of submissions. 2414 * 2415 * @param bool $includenew When true, also counts the submissions with status 'new'. 2416 * @return int number of submissions 2417 */ 2418 public function count_submissions($includenew = false) { 2419 global $DB; 2420 2421 if (!$this->has_instance()) { 2422 return 0; 2423 } 2424 2425 $params = array(); 2426 $sqlnew = ''; 2427 2428 if (!$includenew) { 2429 $sqlnew = ' AND s.status <> :status '; 2430 $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW; 2431 } 2432 2433 if ($this->get_instance()->teamsubmission) { 2434 // We cannot join on the enrolment tables for group submissions (no userid). 2435 $sql = 'SELECT COUNT(DISTINCT s.groupid) 2436 FROM {assign_submission} s 2437 WHERE 2438 s.assignment = :assignid AND 2439 s.timemodified IS NOT NULL AND 2440 s.userid = :groupuserid' . 2441 $sqlnew; 2442 2443 $params['assignid'] = $this->get_instance()->id; 2444 $params['groupuserid'] = 0; 2445 } else { 2446 $currentgroup = groups_get_activity_group($this->get_course_module(), true); 2447 list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true); 2448 2449 $params = array_merge($params, $enrolparams); 2450 $params['assignid'] = $this->get_instance()->id; 2451 2452 $sql = 'SELECT COUNT(DISTINCT s.userid) 2453 FROM {assign_submission} s 2454 JOIN(' . $esql . ') e ON e.id = s.userid 2455 WHERE 2456 s.assignment = :assignid AND 2457 s.timemodified IS NOT NULL ' . 2458 $sqlnew; 2459 2460 } 2461 2462 return $DB->count_records_sql($sql, $params); 2463 } 2464 2465 /** 2466 * Load a count of submissions with a specified status. 2467 * 2468 * @param string $status The submission status - should match one of the constants 2469 * @param mixed $currentgroup int|null the group for counting (if null the function will determine it) 2470 * @return int number of matching submissions 2471 */ 2472 public function count_submissions_with_status($status, $currentgroup = null) { 2473 global $DB; 2474 2475 if ($currentgroup === null) { 2476 $currentgroup = groups_get_activity_group($this->get_course_module(), true); 2477 } 2478 list($esql, $params) = get_enrolled_sql($this->get_context(), '', $currentgroup, true); 2479 2480 $params['assignid'] = $this->get_instance()->id; 2481 $params['assignid2'] = $this->get_instance()->id; 2482 $params['submissionstatus'] = $status; 2483 2484 if ($this->get_instance()->teamsubmission) { 2485 2486 $groupsstr = ''; 2487 if ($currentgroup != 0) { 2488 // If there is an active group we should only display the current group users groups. 2489 $participants = $this->list_participants($currentgroup, true); 2490 $groups = groups_get_all_groups($this->get_course()->id, 2491 array_keys($participants), 2492 $this->get_instance()->teamsubmissiongroupingid, 2493 'DISTINCT g.id, g.name'); 2494 if (empty($groups)) { 2495 // If $groups is empty it means it is not part of $this->get_instance()->teamsubmissiongroupingid. 2496 // All submissions from students that do not belong to any of teamsubmissiongroupingid groups 2497 // count towards groupid = 0. Setting to true as only '0' key matters. 2498 $groups = [true]; 2499 } 2500 list($groupssql, $groupsparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED); 2501 $groupsstr = 's.groupid ' . $groupssql . ' AND'; 2502 $params = $params + $groupsparams; 2503 } 2504 $sql = 'SELECT COUNT(s.groupid) 2505 FROM {assign_submission} s 2506 WHERE 2507 s.latest = 1 AND 2508 s.assignment = :assignid AND 2509 s.timemodified IS NOT NULL AND 2510 s.userid = :groupuserid AND ' 2511 . $groupsstr . ' 2512 s.status = :submissionstatus'; 2513 $params['groupuserid'] = 0; 2514 } else { 2515 $sql = 'SELECT COUNT(s.userid) 2516 FROM {assign_submission} s 2517 JOIN(' . $esql . ') e ON e.id = s.userid 2518 WHERE 2519 s.latest = 1 AND 2520 s.assignment = :assignid AND 2521 s.timemodified IS NOT NULL AND 2522 s.status = :submissionstatus'; 2523 2524 } 2525 2526 return $DB->count_records_sql($sql, $params); 2527 } 2528 2529 /** 2530 * Utility function to get the userid for every row in the grading table 2531 * so the order can be frozen while we iterate it. 2532 * 2533 * @param boolean $cached If true, the cached list from the session could be returned. 2534 * @param string $useridlistid String value used for caching the participant list. 2535 * @return array An array of userids 2536 */ 2537 protected function get_grading_userid_list($cached = false, $useridlistid = '') { 2538 global $SESSION; 2539 2540 if ($cached) { 2541 if (empty($useridlistid)) { 2542 $useridlistid = $this->get_useridlist_key_id(); 2543 } 2544 $useridlistkey = $this->get_useridlist_key($useridlistid); 2545 if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) { 2546 $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list(false, ''); 2547 } 2548 return $SESSION->mod_assign_useridlist[$useridlistkey]; 2549 } 2550 $filter = get_user_preferences('assign_filter', ''); 2551 $table = new assign_grading_table($this, 0, $filter, 0, false); 2552 2553 $useridlist = $table->get_column_data('userid'); 2554 2555 return $useridlist; 2556 } 2557 2558 /** 2559 * Finds all assignment notifications that have yet to be mailed out, and mails them. 2560 * 2561 * Cron function to be run periodically according to the moodle cron. 2562 * 2563 * @return bool 2564 */ 2565 public static function cron() { 2566 global $DB; 2567 2568 // Only ever send a max of one days worth of updates. 2569 $yesterday = time() - (24 * 3600); 2570 $timenow = time(); 2571 $task = \core\task\manager::get_scheduled_task(mod_assign\task\cron_task::class); 2572 $lastruntime = $task->get_last_run_time(); 2573 2574 // Collect all submissions that require mailing. 2575 // Submissions are included if all are true: 2576 // - The assignment is visible in the gradebook. 2577 // - No previous notification has been sent. 2578 // - The grader was a real user, not an automated process. 2579 // - The grade was updated in the past 24 hours. 2580 // - If marking workflow is enabled, the workflow state is at 'released'. 2581 $sql = "SELECT g.id as gradeid, a.course, a.name, a.blindmarking, a.revealidentities, a.hidegrader, 2582 g.*, g.timemodified as lastmodified, cm.id as cmid, um.id as recordid 2583 FROM {assign} a 2584 JOIN {assign_grades} g ON g.assignment = a.id 2585 LEFT JOIN {assign_user_flags} uf ON uf.assignment = a.id AND uf.userid = g.userid 2586 JOIN {course_modules} cm ON cm.course = a.course AND cm.instance = a.id 2587 JOIN {modules} md ON md.id = cm.module AND md.name = 'assign' 2588 JOIN {grade_items} gri ON gri.iteminstance = a.id AND gri.courseid = a.course AND gri.itemmodule = md.name 2589 LEFT JOIN {assign_user_mapping} um ON g.id = um.userid AND um.assignment = a.id 2590 WHERE (a.markingworkflow = 0 OR (a.markingworkflow = 1 AND uf.workflowstate = :wfreleased)) AND 2591 g.grader > 0 AND uf.mailed = 0 AND gri.hidden = 0 AND 2592 g.timemodified >= :yesterday AND g.timemodified <= :today 2593 ORDER BY a.course, cm.id"; 2594 2595 $params = array( 2596 'yesterday' => $yesterday, 2597 'today' => $timenow, 2598 'wfreleased' => ASSIGN_MARKING_WORKFLOW_STATE_RELEASED, 2599 ); 2600 $submissions = $DB->get_records_sql($sql, $params); 2601 2602 if (!empty($submissions)) { 2603 2604 mtrace('Processing ' . count($submissions) . ' assignment submissions ...'); 2605 2606 // Preload courses we are going to need those. 2607 $courseids = array(); 2608 foreach ($submissions as $submission) { 2609 $courseids[] = $submission->course; 2610 } 2611 2612 // Filter out duplicates. 2613 $courseids = array_unique($courseids); 2614 $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); 2615 list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); 2616 $sql = 'SELECT c.*, ' . $ctxselect . 2617 ' FROM {course} c 2618 LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel 2619 WHERE c.id ' . $courseidsql; 2620 2621 $params['contextlevel'] = CONTEXT_COURSE; 2622 $courses = $DB->get_records_sql($sql, $params); 2623 2624 // Clean up... this could go on for a while. 2625 unset($courseids); 2626 unset($ctxselect); 2627 unset($courseidsql); 2628 unset($params); 2629 2630 // Message students about new feedback. 2631 foreach ($submissions as $submission) { 2632 2633 mtrace("Processing assignment submission $submission->id ..."); 2634 2635 // Do not cache user lookups - could be too many. 2636 if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) { 2637 mtrace('Could not find user ' . $submission->userid); 2638 continue; 2639 } 2640 2641 // Use a cache to prevent the same DB queries happening over and over. 2642 if (!array_key_exists($submission->course, $courses)) { 2643 mtrace('Could not find course ' . $submission->course); 2644 continue; 2645 } 2646 $course = $courses[$submission->course]; 2647 if (isset($course->ctxid)) { 2648 // Context has not yet been preloaded. Do so now. 2649 context_helper::preload_from_record($course); 2650 } 2651 2652 // Override the language and timezone of the "current" user, so that 2653 // mail is customised for the receiver. 2654 cron_setup_user($user, $course); 2655 2656 // Context lookups are already cached. 2657 $coursecontext = context_course::instance($course->id); 2658 if (!is_enrolled($coursecontext, $user->id)) { 2659 $courseshortname = format_string($course->shortname, 2660 true, 2661 array('context' => $coursecontext)); 2662 mtrace(fullname($user) . ' not an active participant in ' . $courseshortname); 2663 continue; 2664 } 2665 2666 if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) { 2667 mtrace('Could not find grader ' . $submission->grader); 2668 continue; 2669 } 2670 2671 $modinfo = get_fast_modinfo($course, $user->id); 2672 $cm = $modinfo->get_cm($submission->cmid); 2673 // Context lookups are already cached. 2674 $contextmodule = context_module::instance($cm->id); 2675 2676 if (!$cm->uservisible) { 2677 // Hold mail notification for assignments the user cannot access until later. 2678 continue; 2679 } 2680 2681 // Notify the student. Default to the non-anon version. 2682 $messagetype = 'feedbackavailable'; 2683 // Message type needs 'anon' if "hidden grading" is enabled and the student 2684 // doesn't have permission to see the grader. 2685 if ($submission->hidegrader && !has_capability('mod/assign:showhiddengrader', $contextmodule, $user)) { 2686 $messagetype = 'feedbackavailableanon'; 2687 // There's no point in having an "anonymous grader" if the notification email 2688 // comes from them. Send the email from the noreply user instead. 2689 $grader = core_user::get_noreply_user(); 2690 } 2691 2692 $eventtype = 'assign_notification'; 2693 $updatetime = $submission->lastmodified; 2694 $modulename = get_string('modulename', 'assign'); 2695 2696 $uniqueid = 0; 2697 if ($submission->blindmarking && !$submission->revealidentities) { 2698 if (empty($submission->recordid)) { 2699 $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $grader->id); 2700 } else { 2701 $uniqueid = $submission->recordid; 2702 } 2703 } 2704 $showusers = $submission->blindmarking && !$submission->revealidentities; 2705 self::send_assignment_notification($grader, 2706 $user, 2707 $messagetype, 2708 $eventtype, 2709 $updatetime, 2710 $cm, 2711 $contextmodule, 2712 $course, 2713 $modulename, 2714 $submission->name, 2715 $showusers, 2716 $uniqueid); 2717 2718 $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment)); 2719 if ($flags) { 2720 $flags->mailed = 1; 2721 $DB->update_record('assign_user_flags', $flags); 2722 } else { 2723 $flags = new stdClass(); 2724 $flags->userid = $user->id; 2725 $flags->assignment = $submission->assignment; 2726 $flags->mailed = 1; 2727 $DB->insert_record('assign_user_flags', $flags); 2728 } 2729 2730 mtrace('Done'); 2731 } 2732 mtrace('Done processing ' . count($submissions) . ' assignment submissions'); 2733 2734 cron_setup_user(); 2735 2736 // Free up memory just to be sure. 2737 unset($courses); 2738 } 2739 2740 // Update calendar events to provide a description. 2741 $sql = 'SELECT id 2742 FROM {assign} 2743 WHERE 2744 allowsubmissionsfromdate >= :lastruntime AND 2745 allowsubmissionsfromdate <= :timenow AND 2746 alwaysshowdescription = 0'; 2747 $params = array('lastruntime' => $lastruntime, 'timenow' => $timenow); 2748 $newlyavailable = $DB->get_records_sql($sql, $params); 2749 foreach ($newlyavailable as $record) { 2750 $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST); 2751 $context = context_module::instance($cm->id); 2752 2753 $assignment = new assign($context, null, null); 2754 $assignment->update_calendar($cm->id); 2755 } 2756 2757 return true; 2758 } 2759 2760 /** 2761 * Mark in the database that this grade record should have an update notification sent by cron. 2762 * 2763 * @param stdClass $grade a grade record keyed on id 2764 * @param bool $mailedoverride when true, flag notification to be sent again. 2765 * @return bool true for success 2766 */ 2767 public function notify_grade_modified($grade, $mailedoverride = false) { 2768 global $DB; 2769 2770 $flags = $this->get_user_flags($grade->userid, true); 2771 if ($flags->mailed != 1 || $mailedoverride) { 2772 $flags->mailed = 0; 2773 } 2774 2775 return $this->update_user_flags($flags); 2776 } 2777 2778 /** 2779 * Update user flags for this user in this assignment. 2780 * 2781 * @param stdClass $flags a flags record keyed on id 2782 * @return bool true for success 2783 */ 2784 public function update_user_flags($flags) { 2785 global $DB; 2786 if ($flags->userid <= 0 || $flags->assignment <= 0 || $flags->id <= 0) { 2787 return false; 2788 } 2789 2790 $result = $DB->update_record('assign_user_flags', $flags); 2791 return $result; 2792 } 2793 2794 /** 2795 * Update a grade in the grade table for the assignment and in the gradebook. 2796 * 2797 * @param stdClass $grade a grade record keyed on id 2798 * @param bool $reopenattempt If the attempt reopen method is manual, allow another attempt at this assignment. 2799 * @return bool true for success 2800 */ 2801 public function update_grade($grade, $reopenattempt = false) { 2802 global $DB; 2803 2804 $grade->timemodified = time(); 2805 2806 if (!empty($grade->workflowstate)) { 2807 $validstates = $this->get_marking_workflow_states_for_current_user(); 2808 if (!array_key_exists($grade->workflowstate, $validstates)) { 2809 return false; 2810 } 2811 } 2812 2813 if ($grade->grade && $grade->grade != -1) { 2814 if ($this->get_instance()->grade > 0) { 2815 if (!is_numeric($grade->grade)) { 2816 return false; 2817 } else if ($grade->grade > $this->get_instance()->grade) { 2818 return false; 2819 } else if ($grade->grade < 0) { 2820 return false; 2821 } 2822 } else { 2823 // This is a scale. 2824 if ($scale = $DB->get_record('scale', array('id' => -($this->get_instance()->grade)))) { 2825 $scaleoptions = make_menu_from_list($scale->scale); 2826 if (!array_key_exists((int) $grade->grade, $scaleoptions)) { 2827 return false; 2828 } 2829 } 2830 } 2831 } 2832 2833 if (empty($grade->attemptnumber)) { 2834 // Set it to the default. 2835 $grade->attemptnumber = 0; 2836 } 2837 $DB->update_record('assign_grades', $grade); 2838 2839 $submission = null; 2840 if ($this->get_instance()->teamsubmission) { 2841 if (isset($this->mostrecentteamsubmission)) { 2842 $submission = $this->mostrecentteamsubmission; 2843 } else { 2844 $submission = $this->get_group_submission($grade->userid, 0, false); 2845 } 2846 } else { 2847 $submission = $this->get_user_submission($grade->userid, false); 2848 } 2849 2850 // Only push to gradebook if the update is for the most recent attempt. 2851 if ($submission && $submission->attemptnumber != $grade->attemptnumber) { 2852 return true; 2853 } 2854 2855 if ($this->gradebook_item_update(null, $grade)) { 2856 \mod_assign\event\submission_graded::create_from_grade($this, $grade)->trigger(); 2857 } 2858 2859 // If the conditions are met, allow another attempt. 2860 if ($submission) { 2861 $this->reopen_submission_if_required($grade->userid, 2862 $submission, 2863 $reopenattempt); 2864 } 2865 2866 return true; 2867 } 2868 2869 /** 2870 * View the grant extension date page. 2871 * 2872 * Uses url parameters 'userid' 2873 * or from parameter 'selectedusers' 2874 * 2875 * @param moodleform $mform - Used for validation of the submitted data 2876 * @return string 2877 */ 2878 protected function view_grant_extension($mform) { 2879 global $CFG; 2880 require_once($CFG->dirroot . '/mod/assign/extensionform.php'); 2881 2882 $o = ''; 2883 2884 $data = new stdClass(); 2885 $data->id = $this->get_course_module()->id; 2886 2887 $formparams = array( 2888 'instance' => $this->get_instance(), 2889 'assign' => $this 2890 ); 2891 2892 $users = optional_param('userid', 0, PARAM_INT); 2893 if (!$users) { 2894 $users = required_param('selectedusers', PARAM_SEQUENCE); 2895 } 2896 $userlist = explode(',', $users); 2897 2898 $keys = array('duedate', 'cutoffdate', 'allowsubmissionsfromdate'); 2899 $maxoverride = array('allowsubmissionsfromdate' => 0, 'duedate' => 0, 'cutoffdate' => 0); 2900 foreach ($userlist as $userid) { 2901 // To validate extension date with users overrides. 2902 $override = $this->override_exists($userid); 2903 foreach ($keys as $key) { 2904 if ($override->{$key}) { 2905 if ($maxoverride[$key] < $override->{$key}) { 2906 $maxoverride[$key] = $override->{$key}; 2907 } 2908 } else if ($maxoverride[$key] < $this->get_instance()->{$key}) { 2909 $maxoverride[$key] = $this->get_instance()->{$key}; 2910 } 2911 } 2912 } 2913 foreach ($keys as $key) { 2914 if ($maxoverride[$key]) { 2915 $this->get_instance()->{$key} = $maxoverride[$key]; 2916 } 2917 } 2918 2919 $formparams['userlist'] = $userlist; 2920 2921 $data->selectedusers = $users; 2922 $data->userid = 0; 2923 2924 if (empty($mform)) { 2925 $mform = new mod_assign_extension_form(null, $formparams); 2926 } 2927 $mform->set_data($data); 2928 $header = new assign_header($this->get_instance(), 2929 $this->get_context(), 2930 $this->show_intro(), 2931 $this->get_course_module()->id, 2932 get_string('grantextension', 'assign')); 2933 $o .= $this->get_renderer()->render($header); 2934 $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform)); 2935 $o .= $this->view_footer(); 2936 return $o; 2937 } 2938 2939 /** 2940 * Get a list of the users in the same group as this user. 2941 * 2942 * @param int $groupid The id of the group whose members we want or 0 for the default group 2943 * @param bool $onlyids Whether to retrieve only the user id's 2944 * @param bool $excludesuspended Whether to exclude suspended users 2945 * @return array The users (possibly id's only) 2946 */ 2947 public function get_submission_group_members($groupid, $onlyids, $excludesuspended = false) { 2948 $members = array(); 2949 if ($groupid != 0) { 2950 $allusers = $this->list_participants($groupid, $onlyids); 2951 foreach ($allusers as $user) { 2952 if ($this->get_submission_group($user->id)) { 2953 $members[] = $user; 2954 } 2955 } 2956 } else { 2957 $allusers = $this->list_participants(null, $onlyids); 2958 foreach ($allusers as $user) { 2959 if ($this->get_submission_group($user->id) == null) { 2960 $members[] = $user; 2961 } 2962 } 2963 } 2964 // Exclude suspended users, if user can't see them. 2965 if ($excludesuspended || !has_capability('moodle/course:viewsuspendedusers', $this->context)) { 2966 foreach ($members as $key => $member) { 2967 if (!$this->is_active_user($member->id)) { 2968 unset($members[$key]); 2969 } 2970 } 2971 } 2972 2973 return $members; 2974 } 2975 2976 /** 2977 * Get a list of the users in the same group as this user that have not submitted the assignment. 2978 * 2979 * @param int $groupid The id of the group whose members we want or 0 for the default group 2980 * @param bool $onlyids Whether to retrieve only the user id's 2981 * @return array The users (possibly id's only) 2982 */ 2983 public function get_submission_group_members_who_have_not_submitted($groupid, $onlyids) { 2984 $instance = $this->get_instance(); 2985 if (!$instance->teamsubmission || !$instance->requireallteammemberssubmit) { 2986 return array(); 2987 } 2988 $members = $this->get_submission_group_members($groupid, $onlyids); 2989 2990 foreach ($members as $id => $member) { 2991 $submission = $this->get_user_submission($member->id, false); 2992 if ($submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) { 2993 unset($members[$id]); 2994 } else { 2995 if ($this->is_blind_marking()) { 2996 $members[$id]->alias = get_string('hiddenuser', 'assign') . 2997 $this->get_uniqueid_for_user($id); 2998 } 2999 } 3000 } 3001 return $members; 3002 } 3003 3004 /** 3005 * Load the group submission object for a particular user, optionally creating it if required. 3006 * 3007 * @param int $userid The id of the user whose submission we want 3008 * @param int $groupid The id of the group for this user - may be 0 in which 3009 * case it is determined from the userid. 3010 * @param bool $create If set to true a new submission object will be created in the database 3011 * with the status set to "new". 3012 * @param int $attemptnumber - -1 means the latest attempt 3013 * @return stdClass The submission 3014 */ 3015 public function get_group_submission($userid, $groupid, $create, $attemptnumber=-1) { 3016 global $DB; 3017 3018 if ($groupid == 0) { 3019 $group = $this->get_submission_group($userid); 3020 if ($group) { 3021 $groupid = $group->id; 3022 } 3023 } 3024 3025 // Now get the group submission. 3026 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0); 3027 if ($attemptnumber >= 0) { 3028 $params['attemptnumber'] = $attemptnumber; 3029 } 3030 3031 // Only return the row with the highest attemptnumber. 3032 $submission = null; 3033 $submissions = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', '*', 0, 1); 3034 if ($submissions) { 3035 $submission = reset($submissions); 3036 } 3037 3038 if ($submission) { 3039 return $submission; 3040 } 3041 if ($create) { 3042 $submission = new stdClass(); 3043 $submission->assignment = $this->get_instance()->id; 3044 $submission->userid = 0; 3045 $submission->groupid = $groupid; 3046 $submission->timecreated = time(); 3047 $submission->timemodified = $submission->timecreated; 3048 if ($attemptnumber >= 0) { 3049 $submission->attemptnumber = $attemptnumber; 3050 } else { 3051 $submission->attemptnumber = 0; 3052 } 3053 // Work out if this is the latest submission. 3054 $submission->latest = 0; 3055 $params = array('assignment'=>$this->get_instance()->id, 'groupid'=>$groupid, 'userid'=>0); 3056 if ($attemptnumber == -1) { 3057 // This is a new submission so it must be the latest. 3058 $submission->latest = 1; 3059 } else { 3060 // We need to work this out. 3061 $result = $DB->get_records('assign_submission', $params, 'attemptnumber DESC', 'attemptnumber', 0, 1); 3062 if ($result) { 3063 $latestsubmission = reset($result); 3064 } 3065 if (!$latestsubmission || ($attemptnumber == $latestsubmission->attemptnumber)) { 3066 $submission->latest = 1; 3067 } 3068 } 3069 $transaction = $DB->start_delegated_transaction(); 3070 if ($submission->latest) { 3071 // This is the case when we need to set latest to 0 for all the other attempts. 3072 $DB->set_field('assign_submission', 'latest', 0, $params); 3073 } 3074 $submission->status = ASSIGN_SUBMISSION_STATUS_NEW; 3075 $sid = $DB->insert_record('assign_submission', $submission); 3076 $transaction->allow_commit(); 3077 return $DB->get_record('assign_submission', array('id' => $sid)); 3078 } 3079 return false; 3080 } 3081 3082 /** 3083 * View a summary listing of all assignments in the current course. 3084 * 3085 * @return string 3086 */ 3087 private function view_course_index() { 3088 global $USER; 3089 3090 $o = ''; 3091 3092 $course = $this->get_course(); 3093 $strplural = get_string('modulenameplural', 'assign'); 3094 3095 if (!$cms = get_coursemodules_in_course('assign', $course->id, 'm.duedate')) { 3096 $o .= $this->get_renderer()->notification(get_string('thereareno', 'moodle', $strplural)); 3097 $o .= $this->get_renderer()->continue_button(new moodle_url('/course/view.php', array('id' => $course->id))); 3098 return $o; 3099 } 3100 3101 $strsectionname = ''; 3102 $usesections = course_format_uses_sections($course->format); 3103 $modinfo = get_fast_modinfo($course); 3104 3105 if ($usesections) { 3106 $strsectionname = get_string('sectionname', 'format_'.$course->format); 3107 $sections = $modinfo->get_section_info_all(); 3108 } 3109 $courseindexsummary = new assign_course_index_summary($usesections, $strsectionname); 3110 3111 $timenow = time(); 3112 3113 $currentsection = ''; 3114 foreach ($modinfo->instances['assign'] as $cm) { 3115 if (!$cm->uservisible) { 3116 continue; 3117 } 3118 3119 $timedue = $cms[$cm->id]->duedate; 3120 3121 $sectionname = ''; 3122 if ($usesections && $cm->sectionnum) { 3123 $sectionname = get_section_name($course, $sections[$cm->sectionnum]); 3124 } 3125 3126 $submitted = ''; 3127 $context = context_module::instance($cm->id); 3128 3129 $assignment = new assign($context, $cm, $course); 3130 3131 // Apply overrides. 3132 $assignment->update_effective_access($USER->id); 3133 $timedue = $assignment->get_instance()->duedate; 3134 3135 if (has_capability('mod/assign:grade', $context)) { 3136 $submitted = $assignment->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED); 3137 3138 } else if (has_capability('mod/assign:submit', $context)) { 3139 if ($assignment->get_instance()->teamsubmission) { 3140 $usersubmission = $assignment->get_group_submission($USER->id, 0, false); 3141 } else { 3142 $usersubmission = $assignment->get_user_submission($USER->id, false); 3143 } 3144 3145 if (!empty($usersubmission->status)) { 3146 $submitted = get_string('submissionstatus_' . $usersubmission->status, 'assign'); 3147 } else { 3148 $submitted = get_string('submissionstatus_', 'assign'); 3149 } 3150 } 3151 $gradinginfo = grade_get_grades($course->id, 'mod', 'assign', $cm->instance, $USER->id); 3152 if (isset($gradinginfo->items[0]->grades[$USER->id]) && 3153 !$gradinginfo->items[0]->grades[$USER->id]->hidden ) { 3154 $grade = $gradinginfo->items[0]->grades[$USER->id]->str_grade; 3155 } else { 3156 $grade = '-'; 3157 } 3158 3159 $courseindexsummary->add_assign_info($cm->id, $cm->get_formatted_name(), $sectionname, $timedue, $submitted, $grade); 3160 3161 } 3162 3163 $o .= $this->get_renderer()->render($courseindexsummary); 3164 $o .= $this->view_footer(); 3165 3166 return $o; 3167 } 3168 3169 /** 3170 * View a page rendered by a plugin. 3171 * 3172 * Uses url parameters 'pluginaction', 'pluginsubtype', 'plugin', and 'id'. 3173 * 3174 * @return string 3175 */ 3176 protected function view_plugin_page() { 3177 global $USER; 3178 3179 $o = ''; 3180 3181 $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA); 3182 $plugintype = required_param('plugin', PARAM_PLUGIN); 3183 $pluginaction = required_param('pluginaction', PARAM_ALPHA); 3184 3185 $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype); 3186 if (!$plugin) { 3187 print_error('invalidformdata', ''); 3188 return; 3189 } 3190 3191 $o .= $plugin->view_page($pluginaction); 3192 3193 return $o; 3194 } 3195 3196 3197 /** 3198 * This is used for team assignments to get the group for the specified user. 3199 * If the user is a member of multiple or no groups this will return false 3200 * 3201 * @param int $userid The id of the user whose submission we want 3202 * @return mixed The group or false 3203 */ 3204 public function get_submission_group($userid) { 3205 3206 if (isset($this->usersubmissiongroups[$userid])) { 3207 return $this->usersubmissiongroups[$userid]; 3208 } 3209 3210 $groups = $this->get_all_groups($userid); 3211 if (count($groups) != 1) { 3212 $return = false; 3213 } else { 3214 $return = array_pop($groups); 3215 } 3216 3217 // Cache the user submission group. 3218 $this->usersubmissiongroups[$userid] = $return; 3219 3220 return $return; 3221 } 3222 3223 /** 3224 * Gets all groups the user is a member of. 3225 * 3226 * @param int $userid Teh id of the user who's groups we are checking 3227 * @return array The group objects 3228 */ 3229 public function get_all_groups($userid) { 3230 if (isset($this->usergroups[$userid])) { 3231 return $this->usergroups[$userid]; 3232 } 3233 3234 $grouping = $this->get_instance()->teamsubmissiongroupingid; 3235 $return = groups_get_all_groups($this->get_course()->id, $userid, $grouping); 3236 3237 $this->usergroups[$userid] = $return; 3238 3239 return $return; 3240 } 3241 3242 3243 /** 3244 * Display the submission that is used by a plugin. 3245 * 3246 * Uses url parameters 'sid', 'gid' and 'plugin'. 3247 * 3248 * @param string $pluginsubtype 3249 * @return string 3250 */ 3251 protected function view_plugin_content($pluginsubtype) { 3252 $o = ''; 3253 3254 $submissionid = optional_param('sid', 0, PARAM_INT); 3255 $gradeid = optional_param('gid', 0, PARAM_INT); 3256 $plugintype = required_param('plugin', PARAM_PLUGIN); 3257 $item = null; 3258 if ($pluginsubtype == 'assignsubmission') { 3259 $plugin = $this->get_submission_plugin_by_type($plugintype); 3260 if ($submissionid <= 0) { 3261 throw new coding_exception('Submission id should not be 0'); 3262 } 3263 $item = $this->get_submission($submissionid); 3264 3265 // Check permissions. 3266 if (empty($item->userid)) { 3267 // Group submission. 3268 $this->require_view_group_submission($item->groupid); 3269 } else { 3270 $this->require_view_submission($item->userid); 3271 } 3272 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(), 3273 $this->get_context(), 3274 $this->show_intro(), 3275 $this->get_course_module()->id, 3276 $plugin->get_name())); 3277 $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin, 3278 $item, 3279 assign_submission_plugin_submission::FULL, 3280 $this->get_course_module()->id, 3281 $this->get_return_action(), 3282 $this->get_return_params())); 3283 3284 // Trigger event for viewing a submission. 3285 \mod_assign\event\submission_viewed::create_from_submission($this, $item)->trigger(); 3286 3287 } else { 3288 $plugin = $this->get_feedback_plugin_by_type($plugintype); 3289 if ($gradeid <= 0) { 3290 throw new coding_exception('Grade id should not be 0'); 3291 } 3292 $item = $this->get_grade($gradeid); 3293 // Check permissions. 3294 $this->require_view_submission($item->userid); 3295 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(), 3296 $this->get_context(), 3297 $this->show_intro(), 3298 $this->get_course_module()->id, 3299 $plugin->get_name())); 3300 $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin, 3301 $item, 3302 assign_feedback_plugin_feedback::FULL, 3303 $this->get_course_module()->id, 3304 $this->get_return_action(), 3305 $this->get_return_params())); 3306 3307 // Trigger event for viewing feedback. 3308 \mod_assign\event\feedback_viewed::create_from_grade($this, $item)->trigger(); 3309 } 3310 3311 $o .= $this->view_return_links(); 3312 3313 $o .= $this->view_footer(); 3314 3315 return $o; 3316 } 3317 3318 /** 3319 * Rewrite plugin file urls so they resolve correctly in an exported zip. 3320 * 3321 * @param string $text - The replacement text 3322 * @param stdClass $user - The user record 3323 * @param assign_plugin $plugin - The assignment plugin 3324 */ 3325 public function download_rewrite_pluginfile_urls($text, $user, $plugin) { 3326 // The groupname prefix for the urls doesn't depend on the group mode of the assignment instance. 3327 // Rather, it should be determined by checking the group submission settings of the instance, 3328 // which is what download_submission() does when generating the file name prefixes. 3329 $groupname = ''; 3330 if ($this->get_instance()->teamsubmission) { 3331 $submissiongroup = $this->get_submission_group($user->id); 3332 if ($submissiongroup) { 3333 $groupname = $submissiongroup->name . '-'; 3334 } else { 3335 $groupname = get_string('defaultteam', 'assign') . '-'; 3336 } 3337 } 3338 3339 if ($this->is_blind_marking()) { 3340 $prefix = $groupname . get_string('participant', 'assign'); 3341 $prefix = str_replace('_', ' ', $prefix); 3342 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_'); 3343 } else { 3344 $prefix = $groupname . fullname($user); 3345 $prefix = str_replace('_', ' ', $prefix); 3346 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($user->id) . '_'); 3347 } 3348 3349 // Only prefix files if downloadasfolders user preference is NOT set. 3350 if (!get_user_preferences('assign_downloadasfolders', 1)) { 3351 $subtype = $plugin->get_subtype(); 3352 $type = $plugin->get_type(); 3353 $prefix = $prefix . $subtype . '_' . $type . '_'; 3354 } else { 3355 $prefix = ""; 3356 } 3357 $result = str_replace('@@PLUGINFILE@@/', $prefix, $text); 3358 3359 return $result; 3360 } 3361 3362 /** 3363 * Render the content in editor that is often used by plugin. 3364 * 3365 * @param string $filearea 3366 * @param int $submissionid 3367 * @param string $plugintype 3368 * @param string $editor 3369 * @param string $component 3370 * @param bool $shortentext Whether to shorten the text content. 3371 * @return string 3372 */ 3373 public function render_editor_content($filearea, $submissionid, $plugintype, $editor, $component, $shortentext = false) { 3374 global $CFG; 3375 3376 $result = ''; 3377 3378 $plugin = $this->get_submission_plugin_by_type($plugintype); 3379 3380 $text = $plugin->get_editor_text($editor, $submissionid); 3381 if ($shortentext) { 3382 $text = shorten_text($text, 140); 3383 } 3384 $format = $plugin->get_editor_format($editor, $submissionid); 3385 3386 $finaltext = file_rewrite_pluginfile_urls($text, 3387 'pluginfile.php', 3388 $this->get_context()->id, 3389 $component, 3390 $filearea, 3391 $submissionid); 3392 $params = array('overflowdiv' => true, 'context' => $this->get_context()); 3393 $result .= format_text($finaltext, $format, $params); 3394 3395 if ($CFG->enableportfolios && has_capability('mod/assign:exportownsubmission', $this->context)) { 3396 require_once($CFG->libdir . '/portfoliolib.php'); 3397 3398 $button = new portfolio_add_button(); 3399 $portfolioparams = array('cmid' => $this->get_course_module()->id, 3400 'sid' => $submissionid, 3401 'plugin' => $plugintype, 3402 'editor' => $editor, 3403 'area'=>$filearea); 3404 $button->set_callback_options('assign_portfolio_caller', $portfolioparams, 'mod_assign'); 3405 $fs = get_file_storage(); 3406 3407 if ($files = $fs->get_area_files($this->context->id, 3408 $component, 3409 $filearea, 3410 $submissionid, 3411 'timemodified', 3412 false)) { 3413 $button->set_formats(PORTFOLIO_FORMAT_RICHHTML); 3414 } else { 3415 $button->set_formats(PORTFOLIO_FORMAT_PLAINHTML); 3416 } 3417 $result .= $button->to_html(PORTFOLIO_ADD_TEXT_LINK); 3418 } 3419 return $result; 3420 } 3421 3422 /** 3423 * Display a continue page after grading. 3424 * 3425 * @param string $message - The message to display. 3426 * @return string 3427 */ 3428 protected function view_savegrading_result($message) { 3429 $o = ''; 3430 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(), 3431 $this->get_context(), 3432 $this->show_intro(), 3433 $this->get_course_module()->id, 3434 get_string('savegradingresult', 'assign'))); 3435 $gradingresult = new assign_gradingmessage(get_string('savegradingresult', 'assign'), 3436 $message, 3437 $this->get_course_module()->id); 3438 $o .= $this->get_renderer()->render($gradingresult); 3439 $o .= $this->view_footer(); 3440 return $o; 3441 } 3442 /** 3443 * Display a continue page after quickgrading. 3444 * 3445 * @param string $message - The message to display. 3446 * @return string 3447 */ 3448 protected function view_quickgrading_result($message) { 3449 $o = ''; 3450 $o .= $this->get_renderer()->render(new assign_header($this->get_instance(), 3451 $this->get_context(), 3452 $this->show_intro(), 3453 $this->get_course_module()->id, 3454 get_string('quickgradingresult', 'assign'))); 3455 $gradingerror = in_array($message, $this->get_error_messages()); 3456 $lastpage = optional_param('lastpage', null, PARAM_INT); 3457 $gradingresult = new assign_gradingmessage(get_string('quickgradingresult', 'assign'), 3458 $message, 3459 $this->get_course_module()->id, 3460 $gradingerror, 3461 $lastpage); 3462 $o .= $this->get_renderer()->render($gradingresult); 3463 $o .= $this->view_footer(); 3464 return $o; 3465 } 3466 3467 /** 3468 * Display the page footer. 3469 * 3470 * @return string 3471 */ 3472 protected function view_footer() { 3473 // When viewing the footer during PHPUNIT tests a set_state error is thrown. 3474 if (!PHPUNIT_TEST) { 3475 return $this->get_renderer()->render_footer(); 3476 } 3477 3478 return ''; 3479 } 3480 3481 /** 3482 * Throw an error if the permissions to view this users' group submission are missing. 3483 * 3484 * @param int $groupid Group id. 3485 * @throws required_capability_exception 3486 */ 3487 public function require_view_group_submission($groupid) { 3488 if (!$this->can_view_group_submission($groupid)) { 3489 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', ''); 3490 } 3491 } 3492 3493 /** 3494 * Throw an error if the permissions to view this users submission are missing. 3495 * 3496 * @throws required_capability_exception 3497 * @return none 3498 */ 3499 public function require_view_submission($userid) { 3500 if (!$this->can_view_submission($userid)) { 3501 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', ''); 3502 } 3503 } 3504 3505 /** 3506 * Throw an error if the permissions to view grades in this assignment are missing. 3507 * 3508 * @throws required_capability_exception 3509 * @return none 3510 */ 3511 public function require_view_grades() { 3512 if (!$this->can_view_grades()) { 3513 throw new required_capability_exception($this->context, 'mod/assign:viewgrades', 'nopermission', ''); 3514 } 3515 } 3516 3517 /** 3518 * Does this user have view grade or grade permission for this assignment? 3519 * 3520 * @param mixed $groupid int|null when is set to a value, use this group instead calculating it 3521 * @return bool 3522 */ 3523 public function can_view_grades($groupid = null) { 3524 // Permissions check. 3525 if (!has_any_capability(array('mod/assign:viewgrades', 'mod/assign:grade'), $this->context)) { 3526 return false; 3527 } 3528 // Checks for the edge case when user belongs to no groups and groupmode is sep. 3529 if ($this->get_course_module()->effectivegroupmode == SEPARATEGROUPS) { 3530 if ($groupid === null) { 3531 $groupid = groups_get_activity_allowed_groups($this->get_course_module()); 3532 } 3533 $groupflag = has_capability('moodle/site:accessallgroups', $this->get_context()); 3534 $groupflag = $groupflag || !empty($groupid); 3535 return (bool)$groupflag; 3536 } 3537 return true; 3538 } 3539 3540 /** 3541 * Does this user have grade permission for this assignment? 3542 * 3543 * @param int|stdClass $user The object or id of the user who will do the editing (default to current user). 3544 * @return bool 3545 */ 3546 public function can_grade($user = null) { 3547 // Permissions check. 3548 if (!has_capability('mod/assign:grade', $this->context, $user)) { 3549 return false; 3550 } 3551 3552 return true; 3553 } 3554 3555 /** 3556 * Download a zip file of all assignment submissions. 3557 * 3558 * @param array $userids Array of user ids to download assignment submissions in a zip file 3559 * @return string - If an error occurs, this will contain the error page. 3560 */ 3561 protected function download_submissions($userids = false) { 3562 global $CFG, $DB; 3563 3564 // More efficient to load this here. 3565 require_once($CFG->libdir.'/filelib.php'); 3566 3567 // Increase the server timeout to handle the creation and sending of large zip files. 3568 core_php_time_limit::raise(); 3569 3570 $this->require_view_grades(); 3571 3572 // Load all users with submit. 3573 $students = get_enrolled_users($this->context, "mod/assign:submit", null, 'u.*', null, null, null, 3574 $this->show_only_active_users()); 3575 3576 // Build a list of files to zip. 3577 $filesforzipping = array(); 3578 $fs = get_file_storage(); 3579 3580 $groupmode = groups_get_activity_groupmode($this->get_course_module()); 3581 // All users. 3582 $groupid = 0; 3583 $groupname = ''; 3584 if ($groupmode) { 3585 $groupid = groups_get_activity_group($this->get_course_module(), true); 3586 if (!empty($groupid)) { 3587 $groupname = groups_get_group_name($groupid) . '-'; 3588 } 3589 } 3590 3591 // Construct the zip file name. 3592 $filename = clean_filename($this->get_course()->shortname . '-' . 3593 $this->get_instance()->name . '-' . 3594 $groupname.$this->get_course_module()->id . '.zip'); 3595 3596 // Get all the files for each student. 3597 foreach ($students as $student) { 3598 $userid = $student->id; 3599 // Download all assigments submission or only selected users. 3600 if ($userids and !in_array($userid, $userids)) { 3601 continue; 3602 } 3603 3604 if ((groups_is_member($groupid, $userid) or !$groupmode or !$groupid)) { 3605 // Get the plugins to add their own files to the zip. 3606 3607 $submissiongroup = false; 3608 $groupname = ''; 3609 if ($this->get_instance()->teamsubmission) { 3610 $submission = $this->get_group_submission($userid, 0, false); 3611 $submissiongroup = $this->get_submission_group($userid); 3612 if ($submissiongroup) { 3613 $groupname = $submissiongroup->name . '-'; 3614 } else { 3615 $groupname = get_string('defaultteam', 'assign') . '-'; 3616 } 3617 } else { 3618 $submission = $this->get_user_submission($userid, false); 3619 } 3620 3621 if ($this->is_blind_marking()) { 3622 $prefix = str_replace('_', ' ', $groupname . get_string('participant', 'assign')); 3623 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid)); 3624 } else { 3625 $fullname = fullname($student, has_capability('moodle/site:viewfullnames', $this->get_context())); 3626 $prefix = str_replace('_', ' ', $groupname . $fullname); 3627 $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid)); 3628 } 3629 3630 if ($submission) { 3631 $downloadasfolders = get_user_preferences('assign_downloadasfolders', 1); 3632 foreach ($this->submissionplugins as $plugin) { 3633 if ($plugin->is_enabled() && $plugin->is_visible()) { 3634 if ($downloadasfolders) { 3635 // Create a folder for each user for each assignment plugin. 3636 // This is the default behavior for version of Moodle >= 3.1. 3637 $submission->exportfullpath = true; 3638 $pluginfiles = $plugin->get_files($submission, $student); 3639 foreach ($pluginfiles as $zipfilepath => $file) { 3640 $subtype = $plugin->get_subtype(); 3641 $type = $plugin->get_type(); 3642 $zipfilename = basename($zipfilepath); 3643 $prefixedfilename = clean_filename($prefix . 3644 '_' . 3645 $subtype . 3646 '_' . 3647 $type . 3648 '_'); 3649 if ($type == 'file') { 3650 $pathfilename = $prefixedfilename . $file->get_filepath() . $zipfilename; 3651 } else if ($type == 'onlinetext') { 3652 $pathfilename = $prefixedfilename . '/' . $zipfilename; 3653 } else { 3654 $pathfilename = $prefixedfilename . '/' . $zipfilename; 3655 } 3656 $pathfilename = clean_param($pathfilename, PARAM_PATH); 3657 $filesforzipping[$pathfilename] = $file; 3658 } 3659 } else { 3660 // Create a single folder for all users of all assignment plugins. 3661 // This was the default behavior for version of Moodle < 3.1. 3662 $submission->exportfullpath = false; 3663 $pluginfiles = $plugin->get_files($submission, $student); 3664 foreach ($pluginfiles as $zipfilename => $file) { 3665 $subtype = $plugin->get_subtype(); 3666 $type = $plugin->get_type(); 3667 $prefixedfilename = clean_filename($prefix . 3668 '_' . 3669 $subtype . 3670 '_' . 3671 $type . 3672 '_' . 3673 $zipfilename); 3674 $filesforzipping[$prefixedfilename] = $file; 3675 } 3676 } 3677 } 3678 } 3679 } 3680 } 3681 } 3682 $result = ''; 3683 if (count($filesforzipping) == 0) { 3684 $header = new assign_header($this->get_instance(), 3685 $this->get_context(), 3686 '', 3687 $this->get_course_module()->id, 3688 get_string('downloadall', 'assign')); 3689 $result .= $this->get_renderer()->render($header); 3690 $result .= $this->get_renderer()->notification(get_string('nosubmission', 'assign')); 3691 $url = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id, 3692 'action'=>'grading')); 3693 $result .= $this->get_renderer()->continue_button($url); 3694 $result .= $this->view_footer(); 3695 3696 return $result; 3697 } 3698 3699 // Log zip as downloaded. 3700 \mod_assign\event\all_submissions_downloaded::create_from_assign($this)->trigger(); 3701 3702 // Close the session. 3703 \core\session\manager::write_close(); 3704 3705 $zipwriter = \core_files\archive_writer::get_stream_writer($filename, \core_files\archive_writer::ZIP_WRITER); 3706 3707 // Stream the files into the zip. 3708 foreach ($filesforzipping as $pathinzip => $file) { 3709 if ($file instanceof \stored_file) { 3710 // Most of cases are \stored_file. 3711 $zipwriter->add_file_from_stored_file($pathinzip, $file); 3712 } else if (is_array($file)) { 3713 // Save $file as contents, from onlinetext subplugin. 3714 $content = reset($file); 3715 $zipwriter->add_file_from_string($pathinzip, $content); 3716 } 3717 } 3718 3719 // Finish the archive. 3720 $zipwriter->finish(); 3721 exit(); 3722 } 3723 3724 /** 3725 * Util function to add a message to the log. 3726 * 3727 * @deprecated since 2.7 - Use new events system instead. 3728 * (see http://docs.moodle.org/dev/Migrating_logging_calls_in_plugins). 3729 * 3730 * @param string $action The current action 3731 * @param string $info A detailed description of the change. But no more than 255 characters. 3732 * @param string $url The url to the assign module instance. 3733 * @param bool $return If true, returns the arguments, else adds to log. The purpose of this is to 3734 * retrieve the arguments to use them with the new event system (Event 2). 3735 * @return void|array 3736 */ 3737 public function add_to_log($action = '', $info = '', $url='', $return = false) { 3738 global $USER; 3739