Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Functions used by gradebook plugins and reports. 19 * 20 * @package core_grades 21 * @copyright 2009 Petr Skoda and Nicolas Connault 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 require_once($CFG->libdir . '/gradelib.php'); 26 require_once($CFG->dirroot . '/grade/export/lib.php'); 27 28 use \core_grades\output\action_bar; 29 use \core_grades\output\general_action_bar; 30 31 /** 32 * This class iterates over all users that are graded in a course. 33 * Returns detailed info about users and their grades. 34 * 35 * @author Petr Skoda <skodak@moodle.org> 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 */ 38 class graded_users_iterator { 39 40 /** 41 * The couse whose users we are interested in 42 */ 43 protected $course; 44 45 /** 46 * An array of grade items or null if only user data was requested 47 */ 48 protected $grade_items; 49 50 /** 51 * The group ID we are interested in. 0 means all groups. 52 */ 53 protected $groupid; 54 55 /** 56 * A recordset of graded users 57 */ 58 protected $users_rs; 59 60 /** 61 * A recordset of user grades (grade_grade instances) 62 */ 63 protected $grades_rs; 64 65 /** 66 * Array used when moving to next user while iterating through the grades recordset 67 */ 68 protected $gradestack; 69 70 /** 71 * The first field of the users table by which the array of users will be sorted 72 */ 73 protected $sortfield1; 74 75 /** 76 * Should sortfield1 be ASC or DESC 77 */ 78 protected $sortorder1; 79 80 /** 81 * The second field of the users table by which the array of users will be sorted 82 */ 83 protected $sortfield2; 84 85 /** 86 * Should sortfield2 be ASC or DESC 87 */ 88 protected $sortorder2; 89 90 /** 91 * Should users whose enrolment has been suspended be ignored? 92 */ 93 protected $onlyactive = false; 94 95 /** 96 * Enable user custom fields 97 */ 98 protected $allowusercustomfields = false; 99 100 /** 101 * List of suspended users in course. This includes users whose enrolment status is suspended 102 * or enrolment has expired or not started. 103 */ 104 protected $suspendedusers = array(); 105 106 /** 107 * Constructor 108 * 109 * @param object $course A course object 110 * @param array $grade_items array of grade items, if not specified only user info returned 111 * @param int $groupid iterate only group users if present 112 * @param string $sortfield1 The first field of the users table by which the array of users will be sorted 113 * @param string $sortorder1 The order in which the first sorting field will be sorted (ASC or DESC) 114 * @param string $sortfield2 The second field of the users table by which the array of users will be sorted 115 * @param string $sortorder2 The order in which the second sorting field will be sorted (ASC or DESC) 116 */ 117 public function __construct($course, $grade_items=null, $groupid=0, 118 $sortfield1='lastname', $sortorder1='ASC', 119 $sortfield2='firstname', $sortorder2='ASC') { 120 $this->course = $course; 121 $this->grade_items = $grade_items; 122 $this->groupid = $groupid; 123 $this->sortfield1 = $sortfield1; 124 $this->sortorder1 = $sortorder1; 125 $this->sortfield2 = $sortfield2; 126 $this->sortorder2 = $sortorder2; 127 128 $this->gradestack = array(); 129 } 130 131 /** 132 * Initialise the iterator 133 * 134 * @return boolean success 135 */ 136 public function init() { 137 global $CFG, $DB; 138 139 $this->close(); 140 141 export_verify_grades($this->course->id); 142 $course_item = grade_item::fetch_course_item($this->course->id); 143 if ($course_item->needsupdate) { 144 // Can not calculate all final grades - sorry. 145 return false; 146 } 147 148 $coursecontext = context_course::instance($this->course->id); 149 150 list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx'); 151 list($gradebookroles_sql, $params) = $DB->get_in_or_equal(explode(',', $CFG->gradebookroles), SQL_PARAMS_NAMED, 'grbr'); 152 list($enrolledsql, $enrolledparams) = get_enrolled_sql($coursecontext, '', 0, $this->onlyactive); 153 154 $params = array_merge($params, $enrolledparams, $relatedctxparams); 155 156 if ($this->groupid) { 157 $groupsql = "INNER JOIN {groups_members} gm ON gm.userid = u.id"; 158 $groupwheresql = "AND gm.groupid = :groupid"; 159 // $params contents: gradebookroles 160 $params['groupid'] = $this->groupid; 161 } else { 162 $groupsql = ""; 163 $groupwheresql = ""; 164 } 165 166 if (empty($this->sortfield1)) { 167 // We must do some sorting even if not specified. 168 $ofields = ", u.id AS usrt"; 169 $order = "usrt ASC"; 170 171 } else { 172 $ofields = ", u.$this->sortfield1 AS usrt1"; 173 $order = "usrt1 $this->sortorder1"; 174 if (!empty($this->sortfield2)) { 175 $ofields .= ", u.$this->sortfield2 AS usrt2"; 176 $order .= ", usrt2 $this->sortorder2"; 177 } 178 if ($this->sortfield1 != 'id' and $this->sortfield2 != 'id') { 179 // User order MUST be the same in both queries, 180 // must include the only unique user->id if not already present. 181 $ofields .= ", u.id AS usrt"; 182 $order .= ", usrt ASC"; 183 } 184 } 185 186 $userfields = 'u.*'; 187 $customfieldssql = ''; 188 if ($this->allowusercustomfields && !empty($CFG->grade_export_customprofilefields)) { 189 $customfieldscount = 0; 190 $customfieldsarray = grade_helper::get_user_profile_fields($this->course->id, $this->allowusercustomfields); 191 foreach ($customfieldsarray as $field) { 192 if (!empty($field->customid)) { 193 $customfieldssql .= " 194 LEFT JOIN (SELECT * FROM {user_info_data} 195 WHERE fieldid = :cf$customfieldscount) cf$customfieldscount 196 ON u.id = cf$customfieldscount.userid"; 197 $userfields .= ", cf$customfieldscount.data AS customfield_{$field->customid}"; 198 $params['cf'.$customfieldscount] = $field->customid; 199 $customfieldscount++; 200 } 201 } 202 } 203 204 $users_sql = "SELECT $userfields $ofields 205 FROM {user} u 206 JOIN ($enrolledsql) je ON je.id = u.id 207 $groupsql $customfieldssql 208 JOIN ( 209 SELECT DISTINCT ra.userid 210 FROM {role_assignments} ra 211 WHERE ra.roleid $gradebookroles_sql 212 AND ra.contextid $relatedctxsql 213 ) rainner ON rainner.userid = u.id 214 WHERE u.deleted = 0 215 $groupwheresql 216 ORDER BY $order"; 217 $this->users_rs = $DB->get_recordset_sql($users_sql, $params); 218 219 if (!$this->onlyactive) { 220 $context = context_course::instance($this->course->id); 221 $this->suspendedusers = get_suspended_userids($context); 222 } else { 223 $this->suspendedusers = array(); 224 } 225 226 if (!empty($this->grade_items)) { 227 $itemids = array_keys($this->grade_items); 228 list($itemidsql, $grades_params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED, 'items'); 229 $params = array_merge($params, $grades_params); 230 231 $grades_sql = "SELECT g.* $ofields 232 FROM {grade_grades} g 233 JOIN {user} u ON g.userid = u.id 234 JOIN ($enrolledsql) je ON je.id = u.id 235 $groupsql 236 JOIN ( 237 SELECT DISTINCT ra.userid 238 FROM {role_assignments} ra 239 WHERE ra.roleid $gradebookroles_sql 240 AND ra.contextid $relatedctxsql 241 ) rainner ON rainner.userid = u.id 242 WHERE u.deleted = 0 243 AND g.itemid $itemidsql 244 $groupwheresql 245 ORDER BY $order, g.itemid ASC"; 246 $this->grades_rs = $DB->get_recordset_sql($grades_sql, $params); 247 } else { 248 $this->grades_rs = false; 249 } 250 251 return true; 252 } 253 254 /** 255 * Returns information about the next user 256 * @return mixed array of user info, all grades and feedback or null when no more users found 257 */ 258 public function next_user() { 259 if (!$this->users_rs) { 260 return false; // no users present 261 } 262 263 if (!$this->users_rs->valid()) { 264 if ($current = $this->_pop()) { 265 // this is not good - user or grades updated between the two reads above :-( 266 } 267 268 return false; // no more users 269 } else { 270 $user = $this->users_rs->current(); 271 $this->users_rs->next(); 272 } 273 274 // find grades of this user 275 $grade_records = array(); 276 while (true) { 277 if (!$current = $this->_pop()) { 278 break; // no more grades 279 } 280 281 if (empty($current->userid)) { 282 break; 283 } 284 285 if ($current->userid != $user->id) { 286 // grade of the next user, we have all for this user 287 $this->_push($current); 288 break; 289 } 290 291 $grade_records[$current->itemid] = $current; 292 } 293 294 $grades = array(); 295 $feedbacks = array(); 296 297 if (!empty($this->grade_items)) { 298 foreach ($this->grade_items as $grade_item) { 299 if (!isset($feedbacks[$grade_item->id])) { 300 $feedbacks[$grade_item->id] = new stdClass(); 301 } 302 if (array_key_exists($grade_item->id, $grade_records)) { 303 $feedbacks[$grade_item->id]->feedback = $grade_records[$grade_item->id]->feedback; 304 $feedbacks[$grade_item->id]->feedbackformat = $grade_records[$grade_item->id]->feedbackformat; 305 unset($grade_records[$grade_item->id]->feedback); 306 unset($grade_records[$grade_item->id]->feedbackformat); 307 $grades[$grade_item->id] = new grade_grade($grade_records[$grade_item->id], false); 308 } else { 309 $feedbacks[$grade_item->id]->feedback = ''; 310 $feedbacks[$grade_item->id]->feedbackformat = FORMAT_MOODLE; 311 $grades[$grade_item->id] = 312 new grade_grade(array('userid'=>$user->id, 'itemid'=>$grade_item->id), false); 313 } 314 $grades[$grade_item->id]->grade_item = $grade_item; 315 } 316 } 317 318 // Set user suspended status. 319 $user->suspendedenrolment = isset($this->suspendedusers[$user->id]); 320 $result = new stdClass(); 321 $result->user = $user; 322 $result->grades = $grades; 323 $result->feedbacks = $feedbacks; 324 return $result; 325 } 326 327 /** 328 * Close the iterator, do not forget to call this function 329 */ 330 public function close() { 331 if ($this->users_rs) { 332 $this->users_rs->close(); 333 $this->users_rs = null; 334 } 335 if ($this->grades_rs) { 336 $this->grades_rs->close(); 337 $this->grades_rs = null; 338 } 339 $this->gradestack = array(); 340 } 341 342 /** 343 * Should all enrolled users be exported or just those with an active enrolment? 344 * 345 * @param bool $onlyactive True to limit the export to users with an active enrolment 346 */ 347 public function require_active_enrolment($onlyactive = true) { 348 if (!empty($this->users_rs)) { 349 debugging('Calling require_active_enrolment() has no effect unless you call init() again', DEBUG_DEVELOPER); 350 } 351 $this->onlyactive = $onlyactive; 352 } 353 354 /** 355 * Allow custom fields to be included 356 * 357 * @param bool $allow Whether to allow custom fields or not 358 * @return void 359 */ 360 public function allow_user_custom_fields($allow = true) { 361 if ($allow) { 362 $this->allowusercustomfields = true; 363 } else { 364 $this->allowusercustomfields = false; 365 } 366 } 367 368 /** 369 * Add a grade_grade instance to the grade stack 370 * 371 * @param grade_grade $grade Grade object 372 * 373 * @return void 374 */ 375 private function _push($grade) { 376 array_push($this->gradestack, $grade); 377 } 378 379 380 /** 381 * Remove a grade_grade instance from the grade stack 382 * 383 * @return grade_grade current grade object 384 */ 385 private function _pop() { 386 global $DB; 387 if (empty($this->gradestack)) { 388 if (empty($this->grades_rs) || !$this->grades_rs->valid()) { 389 return null; // no grades present 390 } 391 392 $current = $this->grades_rs->current(); 393 394 $this->grades_rs->next(); 395 396 return $current; 397 } else { 398 return array_pop($this->gradestack); 399 } 400 } 401 } 402 403 /** 404 * Print a selection popup form of the graded users in a course. 405 * 406 * @deprecated since 2.0 407 * 408 * @param int $course id of the course 409 * @param string $actionpage The page receiving the data from the popoup form 410 * @param int $userid id of the currently selected user (or 'all' if they are all selected) 411 * @param int $groupid id of requested group, 0 means all 412 * @param int $includeall bool include all option 413 * @param bool $return If true, will return the HTML, otherwise, will print directly 414 * @return null 415 */ 416 function print_graded_users_selector($course, $actionpage, $userid=0, $groupid=0, $includeall=true, $return=false) { 417 global $CFG, $USER, $OUTPUT; 418 return $OUTPUT->render(grade_get_graded_users_select(substr($actionpage, 0, strpos($actionpage, '/')), $course, $userid, $groupid, $includeall)); 419 } 420 421 function grade_get_graded_users_select($report, $course, $userid, $groupid, $includeall) { 422 global $USER, $CFG; 423 424 if (is_null($userid)) { 425 $userid = $USER->id; 426 } 427 $coursecontext = context_course::instance($course->id); 428 $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol); 429 $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol); 430 $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $coursecontext); 431 $menu = array(); // Will be a list of userid => user name 432 $menususpendedusers = array(); // Suspended users go to a separate optgroup. 433 $gui = new graded_users_iterator($course, null, $groupid); 434 $gui->require_active_enrolment($showonlyactiveenrol); 435 $gui->init(); 436 $label = get_string('selectauser', 'grades'); 437 if ($includeall) { 438 $menu[0] = get_string('allusers', 'grades'); 439 $label = get_string('selectalloroneuser', 'grades'); 440 } 441 while ($userdata = $gui->next_user()) { 442 $user = $userdata->user; 443 $userfullname = fullname($user); 444 if ($user->suspendedenrolment) { 445 $menususpendedusers[$user->id] = $userfullname; 446 } else { 447 $menu[$user->id] = $userfullname; 448 } 449 } 450 $gui->close(); 451 452 if ($includeall) { 453 $menu[0] .= " (" . (count($menu) + count($menususpendedusers) - 1) . ")"; 454 } 455 456 if (!empty($menususpendedusers)) { 457 $menu[] = array(get_string('suspendedusers') => $menususpendedusers); 458 } 459 $gpr = new grade_plugin_return(array('type' => 'report', 'course' => $course, 'groupid' => $groupid)); 460 $select = new single_select( 461 new moodle_url('/grade/report/'.$report.'/index.php', $gpr->get_options()), 462 'userid', $menu, $userid 463 ); 464 $select->label = $label; 465 $select->formid = 'choosegradeuser'; 466 return $select; 467 } 468 469 /** 470 * Hide warning about changed grades during upgrade to 2.8. 471 * 472 * @param int $courseid The current course id. 473 */ 474 function hide_natural_aggregation_upgrade_notice($courseid) { 475 unset_config('show_sumofgrades_upgrade_' . $courseid); 476 } 477 478 /** 479 * Hide warning about changed grades during upgrade from 2.8.0-2.8.6 and 2.9.0. 480 * 481 * @param int $courseid The current course id. 482 */ 483 function grade_hide_min_max_grade_upgrade_notice($courseid) { 484 unset_config('show_min_max_grades_changed_' . $courseid); 485 } 486 487 /** 488 * Use the grade min and max from the grade_grade. 489 * 490 * This is reserved for core use after an upgrade. 491 * 492 * @param int $courseid The current course id. 493 */ 494 function grade_upgrade_use_min_max_from_grade_grade($courseid) { 495 grade_set_setting($courseid, 'minmaxtouse', GRADE_MIN_MAX_FROM_GRADE_GRADE); 496 497 grade_force_full_regrading($courseid); 498 // Do this now, because it probably happened to late in the page load to be happen automatically. 499 grade_regrade_final_grades($courseid); 500 } 501 502 /** 503 * Use the grade min and max from the grade_item. 504 * 505 * This is reserved for core use after an upgrade. 506 * 507 * @param int $courseid The current course id. 508 */ 509 function grade_upgrade_use_min_max_from_grade_item($courseid) { 510 grade_set_setting($courseid, 'minmaxtouse', GRADE_MIN_MAX_FROM_GRADE_ITEM); 511 512 grade_force_full_regrading($courseid); 513 // Do this now, because it probably happened to late in the page load to be happen automatically. 514 grade_regrade_final_grades($courseid); 515 } 516 517 /** 518 * Hide warning about changed grades during upgrade to 2.8. 519 * 520 * @param int $courseid The current course id. 521 */ 522 function hide_aggregatesubcats_upgrade_notice($courseid) { 523 unset_config('show_aggregatesubcats_upgrade_' . $courseid); 524 } 525 526 /** 527 * Hide warning about changed grades due to bug fixes 528 * 529 * @param int $courseid The current course id. 530 */ 531 function hide_gradebook_calculations_freeze_notice($courseid) { 532 unset_config('gradebook_calculations_freeze_' . $courseid); 533 } 534 535 /** 536 * Print warning about changed grades during upgrade to 2.8. 537 * 538 * @param int $courseid The current course id. 539 * @param context $context The course context. 540 * @param string $thispage The relative path for the current page. E.g. /grade/report/user/index.php 541 * @param boolean $return return as string 542 * 543 * @return nothing or string if $return true 544 */ 545 function print_natural_aggregation_upgrade_notice($courseid, $context, $thispage, $return=false) { 546 global $CFG, $OUTPUT; 547 $html = ''; 548 549 // Do not do anything if they cannot manage the grades of this course. 550 if (!has_capability('moodle/grade:manage', $context)) { 551 return $html; 552 } 553 554 $hidesubcatswarning = optional_param('seenaggregatesubcatsupgradedgrades', false, PARAM_BOOL) && confirm_sesskey(); 555 $showsubcatswarning = get_config('core', 'show_aggregatesubcats_upgrade_' . $courseid); 556 $hidenaturalwarning = optional_param('seensumofgradesupgradedgrades', false, PARAM_BOOL) && confirm_sesskey(); 557 $shownaturalwarning = get_config('core', 'show_sumofgrades_upgrade_' . $courseid); 558 559 $hideminmaxwarning = optional_param('seenminmaxupgradedgrades', false, PARAM_BOOL) && confirm_sesskey(); 560 $showminmaxwarning = get_config('core', 'show_min_max_grades_changed_' . $courseid); 561 562 $useminmaxfromgradeitem = optional_param('useminmaxfromgradeitem', false, PARAM_BOOL) && confirm_sesskey(); 563 $useminmaxfromgradegrade = optional_param('useminmaxfromgradegrade', false, PARAM_BOOL) && confirm_sesskey(); 564 565 $minmaxtouse = grade_get_setting($courseid, 'minmaxtouse', $CFG->grade_minmaxtouse); 566 567 $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $courseid); 568 $acceptgradebookchanges = optional_param('acceptgradebookchanges', false, PARAM_BOOL) && confirm_sesskey(); 569 570 // Hide the warning if the user told it to go away. 571 if ($hidenaturalwarning) { 572 hide_natural_aggregation_upgrade_notice($courseid); 573 } 574 // Hide the warning if the user told it to go away. 575 if ($hidesubcatswarning) { 576 hide_aggregatesubcats_upgrade_notice($courseid); 577 } 578 579 // Hide the min/max warning if the user told it to go away. 580 if ($hideminmaxwarning) { 581 grade_hide_min_max_grade_upgrade_notice($courseid); 582 $showminmaxwarning = false; 583 } 584 585 if ($useminmaxfromgradegrade) { 586 // Revert to the new behaviour, we now use the grade_grade for min/max. 587 grade_upgrade_use_min_max_from_grade_grade($courseid); 588 grade_hide_min_max_grade_upgrade_notice($courseid); 589 $showminmaxwarning = false; 590 591 } else if ($useminmaxfromgradeitem) { 592 // Apply the new logic, we now use the grade_item for min/max. 593 grade_upgrade_use_min_max_from_grade_item($courseid); 594 grade_hide_min_max_grade_upgrade_notice($courseid); 595 $showminmaxwarning = false; 596 } 597 598 599 if (!$hidenaturalwarning && $shownaturalwarning) { 600 $message = get_string('sumofgradesupgradedgrades', 'grades'); 601 $hidemessage = get_string('upgradedgradeshidemessage', 'grades'); 602 $urlparams = array( 'id' => $courseid, 603 'seensumofgradesupgradedgrades' => true, 604 'sesskey' => sesskey()); 605 $goawayurl = new moodle_url($thispage, $urlparams); 606 $goawaybutton = $OUTPUT->single_button($goawayurl, $hidemessage, 'get'); 607 $html .= $OUTPUT->notification($message, 'notifysuccess'); 608 $html .= $goawaybutton; 609 } 610 611 if (!$hidesubcatswarning && $showsubcatswarning) { 612 $message = get_string('aggregatesubcatsupgradedgrades', 'grades'); 613 $hidemessage = get_string('upgradedgradeshidemessage', 'grades'); 614 $urlparams = array( 'id' => $courseid, 615 'seenaggregatesubcatsupgradedgrades' => true, 616 'sesskey' => sesskey()); 617 $goawayurl = new moodle_url($thispage, $urlparams); 618 $goawaybutton = $OUTPUT->single_button($goawayurl, $hidemessage, 'get'); 619 $html .= $OUTPUT->notification($message, 'notifysuccess'); 620 $html .= $goawaybutton; 621 } 622 623 if ($showminmaxwarning) { 624 $hidemessage = get_string('upgradedgradeshidemessage', 'grades'); 625 $urlparams = array( 'id' => $courseid, 626 'seenminmaxupgradedgrades' => true, 627 'sesskey' => sesskey()); 628 629 $goawayurl = new moodle_url($thispage, $urlparams); 630 $hideminmaxbutton = $OUTPUT->single_button($goawayurl, $hidemessage, 'get'); 631 $moreinfo = html_writer::link(get_docs_url(get_string('minmaxtouse_link', 'grades')), get_string('moreinfo'), 632 array('target' => '_blank')); 633 634 if ($minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_ITEM) { 635 // Show the message that there were min/max issues that have been resolved. 636 $message = get_string('minmaxupgradedgrades', 'grades') . ' ' . $moreinfo; 637 638 $revertmessage = get_string('upgradedminmaxrevertmessage', 'grades'); 639 $urlparams = array('id' => $courseid, 640 'useminmaxfromgradegrade' => true, 641 'sesskey' => sesskey()); 642 $reverturl = new moodle_url($thispage, $urlparams); 643 $revertbutton = $OUTPUT->single_button($reverturl, $revertmessage, 'get'); 644 645 $html .= $OUTPUT->notification($message); 646 $html .= $revertbutton . $hideminmaxbutton; 647 648 } else if ($minmaxtouse == GRADE_MIN_MAX_FROM_GRADE_GRADE) { 649 // Show the warning that there are min/max issues that have not be resolved. 650 $message = get_string('minmaxupgradewarning', 'grades') . ' ' . $moreinfo; 651 652 $fixmessage = get_string('minmaxupgradefixbutton', 'grades'); 653 $urlparams = array('id' => $courseid, 654 'useminmaxfromgradeitem' => true, 655 'sesskey' => sesskey()); 656 $fixurl = new moodle_url($thispage, $urlparams); 657 $fixbutton = $OUTPUT->single_button($fixurl, $fixmessage, 'get'); 658 659 $html .= $OUTPUT->notification($message); 660 $html .= $fixbutton . $hideminmaxbutton; 661 } 662 } 663 664 if ($gradebookcalculationsfreeze) { 665 if ($acceptgradebookchanges) { 666 // Accept potential changes in grades caused by extra credit bug MDL-49257. 667 hide_gradebook_calculations_freeze_notice($courseid); 668 $courseitem = grade_item::fetch_course_item($courseid); 669 $courseitem->force_regrading(); 670 grade_regrade_final_grades($courseid); 671 672 $html .= $OUTPUT->notification(get_string('gradebookcalculationsuptodate', 'grades'), 'notifysuccess'); 673 } else { 674 // Show the warning that there may be extra credit weights problems. 675 $a = new stdClass(); 676 $a->gradebookversion = $gradebookcalculationsfreeze; 677 if (preg_match('/(\d{8,})/', $CFG->release, $matches)) { 678 $a->currentversion = $matches[1]; 679 } else { 680 $a->currentversion = $CFG->release; 681 } 682 $a->url = get_docs_url('Gradebook_calculation_changes'); 683 $message = get_string('gradebookcalculationswarning', 'grades', $a); 684 685 $fixmessage = get_string('gradebookcalculationsfixbutton', 'grades'); 686 $urlparams = array('id' => $courseid, 687 'acceptgradebookchanges' => true, 688 'sesskey' => sesskey()); 689 $fixurl = new moodle_url($thispage, $urlparams); 690 $fixbutton = $OUTPUT->single_button($fixurl, $fixmessage, 'get'); 691 692 $html .= $OUTPUT->notification($message); 693 $html .= $fixbutton; 694 } 695 } 696 697 if (!empty($html)) { 698 $html = html_writer::tag('div', $html, array('class' => 'core_grades_notices')); 699 } 700 701 if ($return) { 702 return $html; 703 } else { 704 echo $html; 705 } 706 } 707 708 /** 709 * grade_get_plugin_info 710 * 711 * @param int $courseid The course id 712 * @param string $active_type type of plugin on current page - import, export, report or edit 713 * @param string $active_plugin active plugin type - grader, user, cvs, ... 714 * 715 * @return array 716 */ 717 function grade_get_plugin_info($courseid, $active_type, $active_plugin) { 718 global $CFG, $SITE; 719 720 $context = context_course::instance($courseid); 721 722 $plugin_info = array(); 723 $count = 0; 724 $active = ''; 725 $url_prefix = $CFG->wwwroot . '/grade/'; 726 727 // Language strings 728 $plugin_info['strings'] = grade_helper::get_plugin_strings(); 729 730 if ($reports = grade_helper::get_plugins_reports($courseid)) { 731 $plugin_info['report'] = $reports; 732 } 733 734 if ($settings = grade_helper::get_info_manage_settings($courseid)) { 735 $plugin_info['settings'] = $settings; 736 } 737 738 if ($scale = grade_helper::get_info_scales($courseid)) { 739 $plugin_info['scale'] = array('view'=>$scale); 740 } 741 742 if ($outcomes = grade_helper::get_info_outcomes($courseid)) { 743 $plugin_info['outcome'] = $outcomes; 744 } 745 746 if ($letters = grade_helper::get_info_letters($courseid)) { 747 $plugin_info['letter'] = $letters; 748 } 749 750 if ($imports = grade_helper::get_plugins_import($courseid)) { 751 $plugin_info['import'] = $imports; 752 } 753 754 if ($exports = grade_helper::get_plugins_export($courseid)) { 755 $plugin_info['export'] = $exports; 756 } 757 758 // Let other plugins add plugins here so that we get extra tabs 759 // in the gradebook. 760 $callbacks = get_plugins_with_function('extend_gradebook_plugininfo', 'lib.php'); 761 foreach ($callbacks as $plugins) { 762 foreach ($plugins as $pluginfunction) { 763 $plugin_info = $pluginfunction($plugin_info, $courseid); 764 } 765 } 766 767 foreach ($plugin_info as $plugin_type => $plugins) { 768 if (!empty($plugins->id) && $active_plugin == $plugins->id) { 769 $plugin_info['strings']['active_plugin_str'] = $plugins->string; 770 break; 771 } 772 foreach ($plugins as $plugin) { 773 if (is_a($plugin, 'grade_plugin_info')) { 774 if ($active_plugin == $plugin->id) { 775 $plugin_info['strings']['active_plugin_str'] = $plugin->string; 776 } 777 } 778 } 779 } 780 781 return $plugin_info; 782 } 783 784 /** 785 * A simple class containing info about grade plugins. 786 * Can be subclassed for special rules 787 * 788 * @package core_grades 789 * @copyright 2009 Nicolas Connault 790 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 791 */ 792 class grade_plugin_info { 793 /** 794 * A unique id for this plugin 795 * 796 * @var mixed 797 */ 798 public $id; 799 /** 800 * A URL to access this plugin 801 * 802 * @var mixed 803 */ 804 public $link; 805 /** 806 * The name of this plugin 807 * 808 * @var mixed 809 */ 810 public $string; 811 /** 812 * Another grade_plugin_info object, parent of the current one 813 * 814 * @var mixed 815 */ 816 public $parent; 817 818 /** 819 * Constructor 820 * 821 * @param int $id A unique id for this plugin 822 * @param string $link A URL to access this plugin 823 * @param string $string The name of this plugin 824 * @param object $parent Another grade_plugin_info object, parent of the current one 825 * 826 * @return void 827 */ 828 public function __construct($id, $link, $string, $parent=null) { 829 $this->id = $id; 830 $this->link = $link; 831 $this->string = $string; 832 $this->parent = $parent; 833 } 834 } 835 836 /** 837 * Prints the page headers, breadcrumb trail, page heading, (optional) navigation and for any gradebook page. 838 * All gradebook pages MUST use these functions in favour of the usual print_header(), print_header_simple(), 839 * print_heading() etc. 840 * 841 * @param int $courseid Course id 842 * @param string $active_type The type of the current page (report, settings, 843 * import, export, scales, outcomes, letters) 844 * @param string|null $active_plugin The plugin of the current page (grader, fullview etc...) 845 * @param string|bool $heading The heading of the page. Tries to guess if none is given 846 * @param boolean $return Whether to return (true) or echo (false) the HTML generated by this function 847 * @param string|bool $buttons Additional buttons to display on the page 848 * @param boolean $shownavigation should the gradebook navigation be shown? 849 * @param string|null $headerhelpidentifier The help string identifier if required. 850 * @param string|null $headerhelpcomponent The component for the help string. 851 * @param stdClass|null $user The user object for use with the user context header. 852 * @param actionbar|null $actionbar The actions bar which will be displayed on the page if $shownavigation is set 853 * to true. If $actionbar is not explicitly defined, the general action bar 854 * (\core_grades\output\general_action_bar) will be used by default. 855 * @return string HTML code or nothing if $return == false 856 */ 857 function print_grade_page_head(int $courseid, string $active_type, ?string $active_plugin = null, $heading = false, 858 bool $return = false, $buttons = false, bool $shownavigation = true, ?string $headerhelpidentifier = null, 859 ?string $headerhelpcomponent = null, ?stdClass $user = null, ?action_bar $actionbar = null) { 860 global $CFG, $OUTPUT, $PAGE; 861 862 // Put a warning on all gradebook pages if the course has modules currently scheduled for background deletion. 863 require_once($CFG->dirroot . '/course/lib.php'); 864 if (course_modules_pending_deletion($courseid, true)) { 865 \core\notification::add(get_string('gradesmoduledeletionpendingwarning', 'grades'), 866 \core\output\notification::NOTIFY_WARNING); 867 } 868 869 if ($active_type === 'preferences') { 870 // In Moodle 2.8 report preferences were moved under 'settings'. Allow backward compatibility for 3rd party grade reports. 871 $active_type = 'settings'; 872 } 873 874 $plugin_info = grade_get_plugin_info($courseid, $active_type, $active_plugin); 875 876 // Determine the string of the active plugin 877 $stractive_plugin = ($active_plugin) ? $plugin_info['strings']['active_plugin_str'] : $heading; 878 $stractive_type = $plugin_info['strings'][$active_type]; 879 880 if (empty($plugin_info[$active_type]->id) || !empty($plugin_info[$active_type]->parent)) { 881 $title = $PAGE->course->fullname.': ' . $stractive_type . ': ' . $stractive_plugin; 882 } else { 883 $title = $PAGE->course->fullname.': ' . $stractive_plugin; 884 } 885 886 if ($active_type == 'report') { 887 $PAGE->set_pagelayout('report'); 888 } else { 889 $PAGE->set_pagelayout('admin'); 890 } 891 $PAGE->set_title(get_string('grades') . ': ' . $stractive_type); 892 $PAGE->set_heading($title); 893 $PAGE->set_secondary_active_tab('grades'); 894 895 if ($buttons instanceof single_button) { 896 $buttons = $OUTPUT->render($buttons); 897 } 898 $PAGE->set_button($buttons); 899 if ($courseid != SITEID) { 900 grade_extend_settings($plugin_info, $courseid); 901 } 902 903 // Set the current report as active in the breadcrumbs. 904 if ($active_plugin !== null && $reportnav = $PAGE->settingsnav->find($active_plugin, navigation_node::TYPE_SETTING)) { 905 $reportnav->make_active(); 906 } 907 908 $returnval = $OUTPUT->header(); 909 910 if (!$return) { 911 echo $returnval; 912 } 913 914 // Guess heading if not given explicitly 915 if (!$heading) { 916 $heading = $stractive_plugin; 917 } 918 919 if ($shownavigation) { 920 $renderer = $PAGE->get_renderer('core_grades'); 921 // If the navigation action bar is not explicitly defined, use the general (default) action bar. 922 if (!$actionbar) { 923 $actionbar = new general_action_bar($PAGE->context, $PAGE->url, $active_type, $active_plugin); 924 } 925 926 if ($return) { 927 $returnval .= $renderer->render_action_bar($actionbar); 928 } else { 929 echo $renderer->render_action_bar($actionbar); 930 } 931 } 932 933 $output = ''; 934 // Add a help dialogue box if provided. 935 if (isset($headerhelpidentifier)) { 936 $output = $OUTPUT->heading_with_help($heading, $headerhelpidentifier, $headerhelpcomponent); 937 } else { 938 if (isset($user)) { 939 $output = $OUTPUT->context_header( 940 array( 941 'heading' => html_writer::link(new moodle_url('/user/view.php', array('id' => $user->id, 942 'course' => $courseid)), fullname($user)), 943 'user' => $user, 944 'usercontext' => context_user::instance($user->id) 945 ), 2 946 ); 947 } else { 948 $output = $OUTPUT->heading($heading); 949 } 950 } 951 952 if ($return) { 953 $returnval .= $output; 954 } else { 955 echo $output; 956 } 957 958 $returnval .= print_natural_aggregation_upgrade_notice($courseid, context_course::instance($courseid), $PAGE->url, 959 $return); 960 961 if ($return) { 962 return $returnval; 963 } 964 } 965 966 /** 967 * Utility class used for return tracking when using edit and other forms in grade plugins 968 * 969 * @package core_grades 970 * @copyright 2009 Nicolas Connault 971 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 972 */ 973 class grade_plugin_return { 974 /** 975 * Type of grade plugin (e.g. 'edit', 'report') 976 * 977 * @var string 978 */ 979 public $type; 980 /** 981 * Name of grade plugin (e.g. 'grader', 'overview') 982 * 983 * @var string 984 */ 985 public $plugin; 986 /** 987 * Course id being viewed 988 * 989 * @var int 990 */ 991 public $courseid; 992 /** 993 * Id of user whose information is being viewed/edited 994 * 995 * @var int 996 */ 997 public $userid; 998 /** 999 * Id of group for which information is being viewed/edited 1000 * 1001 * @var int 1002 */ 1003 public $groupid; 1004 /** 1005 * Current page # within output 1006 * 1007 * @var int 1008 */ 1009 public $page; 1010 1011 /** 1012 * Constructor 1013 * 1014 * @param array $params - associative array with return parameters, if not supplied parameter are taken from _GET or _POST 1015 */ 1016 public function __construct($params = []) { 1017 $this->type = optional_param('gpr_type', null, PARAM_SAFEDIR); 1018 $this->plugin = optional_param('gpr_plugin', null, PARAM_PLUGIN); 1019 $this->courseid = optional_param('gpr_courseid', null, PARAM_INT); 1020 $this->userid = optional_param('gpr_userid', null, PARAM_INT); 1021 $this->groupid = optional_param('gpr_groupid', null, PARAM_INT); 1022 $this->page = optional_param('gpr_page', null, PARAM_INT); 1023 1024 foreach ($params as $key => $value) { 1025 if (property_exists($this, $key)) { 1026 $this->$key = $value; 1027 } 1028 } 1029 // Allow course object rather than id to be used to specify course 1030 // - avoid unnecessary use of get_course. 1031 if (array_key_exists('course', $params)) { 1032 $course = $params['course']; 1033 $this->courseid = $course->id; 1034 } else { 1035 $course = null; 1036 } 1037 // If group has been explicitly set in constructor parameters, 1038 // we should respect that. 1039 if (!array_key_exists('groupid', $params)) { 1040 // Otherwise, 'group' in request parameters is a request for a change. 1041 // In that case, or if we have no group at all, we should get groupid from 1042 // groups_get_course_group, which will do some housekeeping as well as 1043 // give us the correct value. 1044 $changegroup = optional_param('group', -1, PARAM_INT); 1045 if ($changegroup !== -1 or (empty($this->groupid) and !empty($this->courseid))) { 1046 if ($course === null) { 1047 $course = get_course($this->courseid); 1048 } 1049 $this->groupid = groups_get_course_group($course, true); 1050 } 1051 } 1052 } 1053 1054 /** 1055 * Old syntax of class constructor. Deprecated in PHP7. 1056 * 1057 * @deprecated since Moodle 3.1 1058 */ 1059 public function grade_plugin_return($params = null) { 1060 debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); 1061 self::__construct($params); 1062 } 1063 1064 /** 1065 * Returns return parameters as options array suitable for buttons. 1066 * @return array options 1067 */ 1068 public function get_options() { 1069 if (empty($this->type)) { 1070 return array(); 1071 } 1072 1073 $params = array(); 1074 1075 if (!empty($this->plugin)) { 1076 $params['plugin'] = $this->plugin; 1077 } 1078 1079 if (!empty($this->courseid)) { 1080 $params['id'] = $this->courseid; 1081 } 1082 1083 if (!empty($this->userid)) { 1084 $params['userid'] = $this->userid; 1085 } 1086 1087 if (!empty($this->groupid)) { 1088 $params['group'] = $this->groupid; 1089 } 1090 1091 if (!empty($this->page)) { 1092 $params['page'] = $this->page; 1093 } 1094 1095 return $params; 1096 } 1097 1098 /** 1099 * Returns return url 1100 * 1101 * @param string $default default url when params not set 1102 * @param array $extras Extra URL parameters 1103 * 1104 * @return string url 1105 */ 1106 public function get_return_url($default, $extras=null) { 1107 global $CFG; 1108 1109 if (empty($this->type) or empty($this->plugin)) { 1110 return $default; 1111 } 1112 1113 $url = $CFG->wwwroot.'/grade/'.$this->type.'/'.$this->plugin.'/index.php'; 1114 $glue = '?'; 1115 1116 if (!empty($this->courseid)) { 1117 $url .= $glue.'id='.$this->courseid; 1118 $glue = '&'; 1119 } 1120 1121 if (!empty($this->userid)) { 1122 $url .= $glue.'userid='.$this->userid; 1123 $glue = '&'; 1124 } 1125 1126 if (!empty($this->groupid)) { 1127 $url .= $glue.'group='.$this->groupid; 1128 $glue = '&'; 1129 } 1130 1131 if (!empty($this->page)) { 1132 $url .= $glue.'page='.$this->page; 1133 $glue = '&'; 1134 } 1135 1136 if (!empty($extras)) { 1137 foreach ($extras as $key=>$value) { 1138 $url .= $glue.$key.'='.$value; 1139 $glue = '&'; 1140 } 1141 } 1142 1143 return $url; 1144 } 1145 1146 /** 1147 * Returns string with hidden return tracking form elements. 1148 * @return string 1149 */ 1150 public function get_form_fields() { 1151 if (empty($this->type)) { 1152 return ''; 1153 } 1154 1155 $result = '<input type="hidden" name="gpr_type" value="'.$this->type.'" />'; 1156 1157 if (!empty($this->plugin)) { 1158 $result .= '<input type="hidden" name="gpr_plugin" value="'.$this->plugin.'" />'; 1159 } 1160 1161 if (!empty($this->courseid)) { 1162 $result .= '<input type="hidden" name="gpr_courseid" value="'.$this->courseid.'" />'; 1163 } 1164 1165 if (!empty($this->userid)) { 1166 $result .= '<input type="hidden" name="gpr_userid" value="'.$this->userid.'" />'; 1167 } 1168 1169 if (!empty($this->groupid)) { 1170 $result .= '<input type="hidden" name="gpr_groupid" value="'.$this->groupid.'" />'; 1171 } 1172 1173 if (!empty($this->page)) { 1174 $result .= '<input type="hidden" name="gpr_page" value="'.$this->page.'" />'; 1175 } 1176 return $result; 1177 } 1178 1179 /** 1180 * Add hidden elements into mform 1181 * 1182 * @param object &$mform moodle form object 1183 * 1184 * @return void 1185 */ 1186 public function add_mform_elements(&$mform) { 1187 if (empty($this->type)) { 1188 return; 1189 } 1190 1191 $mform->addElement('hidden', 'gpr_type', $this->type); 1192 $mform->setType('gpr_type', PARAM_SAFEDIR); 1193 1194 if (!empty($this->plugin)) { 1195 $mform->addElement('hidden', 'gpr_plugin', $this->plugin); 1196 $mform->setType('gpr_plugin', PARAM_PLUGIN); 1197 } 1198 1199 if (!empty($this->courseid)) { 1200 $mform->addElement('hidden', 'gpr_courseid', $this->courseid); 1201 $mform->setType('gpr_courseid', PARAM_INT); 1202 } 1203 1204 if (!empty($this->userid)) { 1205 $mform->addElement('hidden', 'gpr_userid', $this->userid); 1206 $mform->setType('gpr_userid', PARAM_INT); 1207 } 1208 1209 if (!empty($this->groupid)) { 1210 $mform->addElement('hidden', 'gpr_groupid', $this->groupid); 1211 $mform->setType('gpr_groupid', PARAM_INT); 1212 } 1213 1214 if (!empty($this->page)) { 1215 $mform->addElement('hidden', 'gpr_page', $this->page); 1216 $mform->setType('gpr_page', PARAM_INT); 1217 } 1218 } 1219 1220 /** 1221 * Add return tracking params into url 1222 * 1223 * @param moodle_url $url A URL 1224 * @return moodle_url with return tracking params 1225 */ 1226 public function add_url_params(moodle_url $url): moodle_url { 1227 if (empty($this->type)) { 1228 return $url; 1229 } 1230 1231 $url->param('gpr_type', $this->type); 1232 1233 if (!empty($this->plugin)) { 1234 $url->param('gpr_plugin', $this->plugin); 1235 } 1236 1237 if (!empty($this->courseid)) { 1238 $url->param('gpr_courseid' ,$this->courseid); 1239 } 1240 1241 if (!empty($this->userid)) { 1242 $url->param('gpr_userid', $this->userid); 1243 } 1244 1245 if (!empty($this->groupid)) { 1246 $url->param('gpr_groupid', $this->groupid); 1247 } 1248 1249 if (!empty($this->page)) { 1250 $url->param('gpr_page', $this->page); 1251 } 1252 1253 return $url; 1254 } 1255 } 1256 1257 /** 1258 * Function central to gradebook for building and printing the navigation (breadcrumb trail). 1259 * 1260 * @param string $path The path of the calling script (using __FILE__?) 1261 * @param string $pagename The language string to use as the last part of the navigation (non-link) 1262 * @param mixed $id Either a plain integer (assuming the key is 'id') or 1263 * an array of keys and values (e.g courseid => $courseid, itemid...) 1264 * 1265 * @return string 1266 */ 1267 function grade_build_nav($path, $pagename=null, $id=null) { 1268 global $CFG, $COURSE, $PAGE; 1269 1270 $strgrades = get_string('grades', 'grades'); 1271 1272 // Parse the path and build navlinks from its elements 1273 $dirroot_length = strlen($CFG->dirroot) + 1; // Add 1 for the first slash 1274 $path = substr($path, $dirroot_length); 1275 $path = str_replace('\\', '/', $path); 1276 1277 $path_elements = explode('/', $path); 1278 1279 $path_elements_count = count($path_elements); 1280 1281 // First link is always 'grade' 1282 $PAGE->navbar->add($strgrades, new moodle_url('/grade/index.php', array('id'=>$COURSE->id))); 1283 1284 $link = null; 1285 $numberofelements = 3; 1286 1287 // Prepare URL params string 1288 $linkparams = array(); 1289 if (!is_null($id)) { 1290 if (is_array($id)) { 1291 foreach ($id as $idkey => $idvalue) { 1292 $linkparams[$idkey] = $idvalue; 1293 } 1294 } else { 1295 $linkparams['id'] = $id; 1296 } 1297 } 1298 1299 $navlink4 = null; 1300 1301 // Remove file extensions from filenames 1302 foreach ($path_elements as $key => $filename) { 1303 $path_elements[$key] = str_replace('.php', '', $filename); 1304 } 1305 1306 // Second level links 1307 switch ($path_elements[1]) { 1308 case 'edit': // No link 1309 if ($path_elements[3] != 'index.php') { 1310 $numberofelements = 4; 1311 } 1312 break; 1313 case 'import': // No link 1314 break; 1315 case 'export': // No link 1316 break; 1317 case 'report': 1318 // $id is required for this link. Do not print it if $id isn't given 1319 if (!is_null($id)) { 1320 $link = new moodle_url('/grade/report/index.php', $linkparams); 1321 } 1322 1323 if ($path_elements[2] == 'grader') { 1324 $numberofelements = 4; 1325 } 1326 break; 1327 1328 default: 1329 // If this element isn't among the ones already listed above, it isn't supported, throw an error. 1330 debugging("grade_build_nav() doesn't support ". $path_elements[1] . 1331 " as the second path element after 'grade'."); 1332 return false; 1333 } 1334 $PAGE->navbar->add(get_string($path_elements[1], 'grades'), $link); 1335 1336 // Third level links 1337 if (empty($pagename)) { 1338 $pagename = get_string($path_elements[2], 'grades'); 1339 } 1340 1341 switch ($numberofelements) { 1342 case 3: 1343 $PAGE->navbar->add($pagename, $link); 1344 break; 1345 case 4: 1346 if ($path_elements[2] == 'grader' AND $path_elements[3] != 'index.php') { 1347 $PAGE->navbar->add(get_string('pluginname', 'gradereport_grader'), new moodle_url('/grade/report/grader/index.php', $linkparams)); 1348 } 1349 $PAGE->navbar->add($pagename); 1350 break; 1351 } 1352 1353 return ''; 1354 } 1355 1356 /** 1357 * General structure representing grade items in course 1358 * 1359 * @package core_grades 1360 * @copyright 2009 Nicolas Connault 1361 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1362 */ 1363 class grade_structure { 1364 public $context; 1365 1366 public $courseid; 1367 1368 /** 1369 * Reference to modinfo for current course (for performance, to save 1370 * retrieving it from courseid every time). Not actually set except for 1371 * the grade_tree type. 1372 * @var course_modinfo 1373 */ 1374 public $modinfo; 1375 1376 /** 1377 * 1D array of grade items only 1378 */ 1379 public $items; 1380 1381 /** 1382 * Returns icon of element 1383 * 1384 * @param array &$element An array representing an element in the grade_tree 1385 * @param bool $spacerifnone return spacer if no icon found 1386 * 1387 * @return string icon or spacer 1388 */ 1389 public function get_element_icon(&$element, $spacerifnone=false) { 1390 global $CFG, $OUTPUT; 1391 require_once $CFG->libdir.'/filelib.php'; 1392 1393 $outputstr = ''; 1394 1395 // Object holding pix_icon information before instantiation. 1396 $icon = new stdClass(); 1397 $icon->attributes = array( 1398 'class' => 'icon itemicon' 1399 ); 1400 $icon->component = 'moodle'; 1401 1402 $none = true; 1403 switch ($element['type']) { 1404 case 'item': 1405 case 'courseitem': 1406 case 'categoryitem': 1407 $none = false; 1408 1409 $is_course = $element['object']->is_course_item(); 1410 $is_category = $element['object']->is_category_item(); 1411 $is_scale = $element['object']->gradetype == GRADE_TYPE_SCALE; 1412 $is_value = $element['object']->gradetype == GRADE_TYPE_VALUE; 1413 $is_outcome = !empty($element['object']->outcomeid); 1414 1415 if ($element['object']->is_calculated()) { 1416 $icon->pix = 'i/calc'; 1417 $icon->title = s(get_string('calculatedgrade', 'grades')); 1418 1419 } else if (($is_course or $is_category) and ($is_scale or $is_value)) { 1420 if ($category = $element['object']->get_item_category()) { 1421 $aggrstrings = grade_helper::get_aggregation_strings(); 1422 $stragg = $aggrstrings[$category->aggregation]; 1423 1424 $icon->pix = 'i/calc'; 1425 $icon->title = s($stragg); 1426 1427 switch ($category->aggregation) { 1428 case GRADE_AGGREGATE_MEAN: 1429 case GRADE_AGGREGATE_MEDIAN: 1430 case GRADE_AGGREGATE_WEIGHTED_MEAN: 1431 case GRADE_AGGREGATE_WEIGHTED_MEAN2: 1432 case GRADE_AGGREGATE_EXTRACREDIT_MEAN: 1433 $icon->pix = 'i/agg_mean'; 1434 break; 1435 case GRADE_AGGREGATE_SUM: 1436 $icon->pix = 'i/agg_sum'; 1437 break; 1438 } 1439 } 1440 1441 } else if ($element['object']->itemtype == 'mod') { 1442 // Prevent outcomes displaying the same icon as the activity they are attached to. 1443 if ($is_outcome) { 1444 $icon->pix = 'i/outcomes'; 1445 $icon->title = s(get_string('outcome', 'grades')); 1446 } else { 1447 $modinfo = get_fast_modinfo($element['object']->courseid); 1448 $module = $element['object']->itemmodule; 1449 $instanceid = $element['object']->iteminstance; 1450 if (isset($modinfo->instances[$module][$instanceid])) { 1451 $icon->url = $modinfo->instances[$module][$instanceid]->get_icon_url(); 1452 } else { 1453 $icon->pix = 'monologo'; 1454 $icon->component = $element['object']->itemmodule; 1455 } 1456 $icon->title = s(get_string('modulename', $element['object']->itemmodule)); 1457 } 1458 } else if ($element['object']->itemtype == 'manual') { 1459 if ($element['object']->is_outcome_item()) { 1460 $icon->pix = 'i/outcomes'; 1461 $icon->title = s(get_string('outcome', 'grades')); 1462 } else { 1463 $icon->pix = 'i/manual_item'; 1464 $icon->title = s(get_string('manualitem', 'grades')); 1465 } 1466 } 1467 break; 1468 1469 case 'category': 1470 $none = false; 1471 $icon->pix = 'i/folder'; 1472 $icon->title = s(get_string('category', 'grades')); 1473 break; 1474 } 1475 1476 if ($none) { 1477 if ($spacerifnone) { 1478 $outputstr = $OUTPUT->spacer() . ' '; 1479 } 1480 } else if (isset($icon->url)) { 1481 $outputstr = html_writer::img($icon->url, $icon->title, $icon->attributes); 1482 } else { 1483 $outputstr = $OUTPUT->pix_icon($icon->pix, $icon->title, $icon->component, $icon->attributes); 1484 } 1485 1486 return $outputstr; 1487 } 1488 1489 /** 1490 * Returns name of element optionally with icon and link 1491 * 1492 * @param array &$element An array representing an element in the grade_tree 1493 * @param bool $withlink Whether or not this header has a link 1494 * @param bool $icon Whether or not to display an icon with this header 1495 * @param bool $spacerifnone return spacer if no icon found 1496 * @param bool $withdescription Show description if defined by this item. 1497 * @param bool $fulltotal If the item is a category total, returns $categoryname."total" 1498 * instead of "Category total" or "Course total" 1499 * 1500 * @return string header 1501 */ 1502 public function get_element_header(&$element, $withlink = false, $icon = true, $spacerifnone = false, 1503 $withdescription = false, $fulltotal = false) { 1504 $header = ''; 1505 1506 if ($icon) { 1507 $header .= $this->get_element_icon($element, $spacerifnone); 1508 } 1509 1510 $title = $element['object']->get_name($fulltotal); 1511 $titleunescaped = $element['object']->get_name($fulltotal, false); 1512 $header .= $title; 1513 1514 if ($element['type'] != 'item' and $element['type'] != 'categoryitem' and 1515 $element['type'] != 'courseitem') { 1516 return $header; 1517 } 1518 1519 if ($withlink && $url = $this->get_activity_link($element)) { 1520 $a = new stdClass(); 1521 $a->name = get_string('modulename', $element['object']->itemmodule); 1522 $a->title = $titleunescaped; 1523 $title = get_string('linktoactivity', 'grades', $a); 1524 1525 $header = html_writer::link($url, $header, array('title' => $title, 'class' => 'gradeitemheader')); 1526 } else { 1527 $header = html_writer::span($header, 'gradeitemheader', array('title' => $titleunescaped, 'tabindex' => '0')); 1528 } 1529 1530 if ($withdescription) { 1531 $desc = $element['object']->get_description(); 1532 if (!empty($desc)) { 1533 $header .= '<div class="gradeitemdescription">' . s($desc) . '</div><div class="gradeitemdescriptionfiller"></div>'; 1534 } 1535 } 1536 1537 return $header; 1538 } 1539 1540 private function get_activity_link($element) { 1541 global $CFG; 1542 /** @var array static cache of the grade.php file existence flags */ 1543 static $hasgradephp = array(); 1544 1545 $itemtype = $element['object']->itemtype; 1546 $itemmodule = $element['object']->itemmodule; 1547 $iteminstance = $element['object']->iteminstance; 1548 $itemnumber = $element['object']->itemnumber; 1549 1550 // Links only for module items that have valid instance, module and are 1551 // called from grade_tree with valid modinfo 1552 if ($itemtype != 'mod' || !$iteminstance || !$itemmodule || !$this->modinfo) { 1553 return null; 1554 } 1555 1556 // Get $cm efficiently and with visibility information using modinfo 1557 $instances = $this->modinfo->get_instances(); 1558 if (empty($instances[$itemmodule][$iteminstance])) { 1559 return null; 1560 } 1561 $cm = $instances[$itemmodule][$iteminstance]; 1562 1563 // Do not add link if activity is not visible to the current user 1564 if (!$cm->uservisible) { 1565 return null; 1566 } 1567 1568 if (!array_key_exists($itemmodule, $hasgradephp)) { 1569 if (file_exists($CFG->dirroot . '/mod/' . $itemmodule . '/grade.php')) { 1570 $hasgradephp[$itemmodule] = true; 1571 } else { 1572 $hasgradephp[$itemmodule] = false; 1573 } 1574 } 1575 1576 // If module has grade.php, link to that, otherwise view.php 1577 if ($hasgradephp[$itemmodule]) { 1578 $args = array('id' => $cm->id, 'itemnumber' => $itemnumber); 1579 if (isset($element['userid'])) { 1580 $args['userid'] = $element['userid']; 1581 } 1582 return new moodle_url('/mod/' . $itemmodule . '/grade.php', $args); 1583 } else { 1584 return new moodle_url('/mod/' . $itemmodule . '/view.php', array('id' => $cm->id)); 1585 } 1586 } 1587 1588 /** 1589 * Returns URL of a page that is supposed to contain detailed grade analysis 1590 * 1591 * At the moment, only activity modules are supported. The method generates link 1592 * to the module's file grade.php with the parameters id (cmid), itemid, itemnumber, 1593 * gradeid and userid. If the grade.php does not exist, null is returned. 1594 * 1595 * @return moodle_url|null URL or null if unable to construct it 1596 */ 1597 public function get_grade_analysis_url(grade_grade $grade) { 1598 global $CFG; 1599 /** @var array static cache of the grade.php file existence flags */ 1600 static $hasgradephp = array(); 1601 1602 if (empty($grade->grade_item) or !($grade->grade_item instanceof grade_item)) { 1603 throw new coding_exception('Passed grade without the associated grade item'); 1604 } 1605 $item = $grade->grade_item; 1606 1607 if (!$item->is_external_item()) { 1608 // at the moment, only activity modules are supported 1609 return null; 1610 } 1611 if ($item->itemtype !== 'mod') { 1612 throw new coding_exception('Unknown external itemtype: '.$item->itemtype); 1613 } 1614 if (empty($item->iteminstance) or empty($item->itemmodule) or empty($this->modinfo)) { 1615 return null; 1616 } 1617 1618 if (!array_key_exists($item->itemmodule, $hasgradephp)) { 1619 if (file_exists($CFG->dirroot . '/mod/' . $item->itemmodule . '/grade.php')) { 1620 $hasgradephp[$item->itemmodule] = true; 1621 } else { 1622 $hasgradephp[$item->itemmodule] = false; 1623 } 1624 } 1625 1626 if (!$hasgradephp[$item->itemmodule]) { 1627 return null; 1628 } 1629 1630 $instances = $this->modinfo->get_instances(); 1631 if (empty($instances[$item->itemmodule][$item->iteminstance])) { 1632 return null; 1633 } 1634 $cm = $instances[$item->itemmodule][$item->iteminstance]; 1635 if (!$cm->uservisible) { 1636 return null; 1637 } 1638 1639 $url = new moodle_url('/mod/'.$item->itemmodule.'/grade.php', array( 1640 'id' => $cm->id, 1641 'itemid' => $item->id, 1642 'itemnumber' => $item->itemnumber, 1643 'gradeid' => $grade->id, 1644 'userid' => $grade->userid, 1645 )); 1646 1647 return $url; 1648 } 1649 1650 /** 1651 * Returns an action icon leading to the grade analysis page 1652 * 1653 * @param grade_grade $grade 1654 * @return string 1655 */ 1656 public function get_grade_analysis_icon(grade_grade $grade) { 1657 global $OUTPUT; 1658 1659 $url = $this->get_grade_analysis_url($grade); 1660 if (is_null($url)) { 1661 return ''; 1662 } 1663 1664 $title = get_string('gradeanalysis', 'core_grades'); 1665 return $OUTPUT->action_icon($url, new pix_icon('t/preview', ''), null, 1666 ['title' => $title, 'aria-label' => $title]); 1667 } 1668 1669 /** 1670 * Returns the grade eid - the grade may not exist yet. 1671 * 1672 * @param grade_grade $grade_grade A grade_grade object 1673 * 1674 * @return string eid 1675 */ 1676 public function get_grade_eid($grade_grade) { 1677 if (empty($grade_grade->id)) { 1678 return 'n'.$grade_grade->itemid.'u'.$grade_grade->userid; 1679 } else { 1680 return 'g'.$grade_grade->id; 1681 } 1682 } 1683 1684 /** 1685 * Returns the grade_item eid 1686 * @param grade_item $grade_item A grade_item object 1687 * @return string eid 1688 */ 1689 public function get_item_eid($grade_item) { 1690 return 'ig'.$grade_item->id; 1691 } 1692 1693 /** 1694 * Given a grade_tree element, returns an array of parameters 1695 * used to build an icon for that element. 1696 * 1697 * @param array $element An array representing an element in the grade_tree 1698 * 1699 * @return array 1700 */ 1701 public function get_params_for_iconstr($element) { 1702 $strparams = new stdClass(); 1703 $strparams->category = ''; 1704 $strparams->itemname = ''; 1705 $strparams->itemmodule = ''; 1706 1707 if (!method_exists($element['object'], 'get_name')) { 1708 return $strparams; 1709 } 1710 1711 $strparams->itemname = html_to_text($element['object']->get_name()); 1712 1713 // If element name is categorytotal, get the name of the parent category 1714 if ($strparams->itemname == get_string('categorytotal', 'grades')) { 1715 $parent = $element['object']->get_parent_category(); 1716 $strparams->category = $parent->get_name() . ' '; 1717 } else { 1718 $strparams->category = ''; 1719 } 1720 1721 $strparams->itemmodule = null; 1722 if (isset($element['object']->itemmodule)) { 1723 $strparams->itemmodule = $element['object']->itemmodule; 1724 } 1725 return $strparams; 1726 } 1727 1728 /** 1729 * Return a reset icon for the given element. 1730 * 1731 * @param array $element An array representing an element in the grade_tree 1732 * @param object $gpr A grade_plugin_return object 1733 * @param bool $returnactionmenulink return the instance of action_menu_link instead of string 1734 * @return string|action_menu_link 1735 */ 1736 public function get_reset_icon($element, $gpr, $returnactionmenulink = false) { 1737 global $CFG, $OUTPUT; 1738 1739 // Limit to category items set to use the natural weights aggregation method, and users 1740 // with the capability to manage grades. 1741 if ($element['type'] != 'category' || $element['object']->aggregation != GRADE_AGGREGATE_SUM || 1742 !has_capability('moodle/grade:manage', $this->context)) { 1743 return $returnactionmenulink ? null : ''; 1744 } 1745 1746 $str = get_string('resetweights', 'grades', $this->get_params_for_iconstr($element)); 1747 $url = new moodle_url('/grade/edit/tree/action.php', array( 1748 'id' => $this->courseid, 1749 'action' => 'resetweights', 1750 'eid' => $element['eid'], 1751 'sesskey' => sesskey(), 1752 )); 1753 1754 if ($returnactionmenulink) { 1755 return new action_menu_link_secondary($gpr->add_url_params($url), new pix_icon('t/reset', $str), 1756 get_string('resetweightsshort', 'grades')); 1757 } else { 1758 return $OUTPUT->action_icon($gpr->add_url_params($url), new pix_icon('t/reset', $str)); 1759 } 1760 } 1761 1762 /** 1763 * Return edit icon for give element 1764 * 1765 * @param array $element An array representing an element in the grade_tree 1766 * @param object $gpr A grade_plugin_return object 1767 * @param bool $returnactionmenulink return the instance of action_menu_link instead of string 1768 * @return string|action_menu_link 1769 */ 1770 public function get_edit_icon($element, $gpr, $returnactionmenulink = false) { 1771 global $CFG, $OUTPUT; 1772 1773 if (!has_capability('moodle/grade:manage', $this->context)) { 1774 if ($element['type'] == 'grade' and has_capability('moodle/grade:edit', $this->context)) { 1775 // oki - let them override grade 1776 } else { 1777 return $returnactionmenulink ? null : ''; 1778 } 1779 } 1780 1781 static $strfeedback = null; 1782 static $streditgrade = null; 1783 if (is_null($streditgrade)) { 1784 $streditgrade = get_string('editgrade', 'grades'); 1785 $strfeedback = get_string('feedback'); 1786 } 1787 1788 $strparams = $this->get_params_for_iconstr($element); 1789 1790 $object = $element['object']; 1791 1792 switch ($element['type']) { 1793 case 'item': 1794 case 'categoryitem': 1795 case 'courseitem': 1796 $stredit = get_string('editverbose', 'grades', $strparams); 1797 if (empty($object->outcomeid) || empty($CFG->enableoutcomes)) { 1798 $url = new moodle_url('/grade/edit/tree/item.php', 1799 array('courseid' => $this->courseid, 'id' => $object->id)); 1800 } else { 1801 $url = new moodle_url('/grade/edit/tree/outcomeitem.php', 1802 array('courseid' => $this->courseid, 'id' => $object->id)); 1803 } 1804 break; 1805 1806 case 'category': 1807 $stredit = get_string('editverbose', 'grades', $strparams); 1808 $url = new moodle_url('/grade/edit/tree/category.php', 1809 array('courseid' => $this->courseid, 'id' => $object->id)); 1810 break; 1811 1812 case 'grade': 1813 $stredit = $streditgrade; 1814 if (empty($object->id)) { 1815 $url = new moodle_url('/grade/edit/tree/grade.php', 1816 array('courseid' => $this->courseid, 'itemid' => $object->itemid, 'userid' => $object->userid)); 1817 } else { 1818 $url = new moodle_url('/grade/edit/tree/grade.php', 1819 array('courseid' => $this->courseid, 'id' => $object->id)); 1820 } 1821 if (!empty($object->feedback)) { 1822 $feedback = addslashes_js(trim(format_string($object->feedback, $object->feedbackformat))); 1823 } 1824 break; 1825 1826 default: 1827 $url = null; 1828 } 1829 1830 if ($url) { 1831 if ($returnactionmenulink) { 1832 return new action_menu_link_secondary($gpr->add_url_params($url), 1833 new pix_icon('t/edit', $stredit), 1834 get_string('editsettings')); 1835 } else { 1836 return $OUTPUT->action_icon($gpr->add_url_params($url), new pix_icon('t/edit', $stredit)); 1837 } 1838 1839 } else { 1840 return $returnactionmenulink ? null : ''; 1841 } 1842 } 1843 1844 /** 1845 * Return hiding icon for give element 1846 * 1847 * @param array $element An array representing an element in the grade_tree 1848 * @param object $gpr A grade_plugin_return object 1849 * @param bool $returnactionmenulink return the instance of action_menu_link instead of string 1850 * @return string|action_menu_link 1851 */ 1852 public function get_hiding_icon($element, $gpr, $returnactionmenulink = false) { 1853 global $CFG, $OUTPUT; 1854 1855 if (!$element['object']->can_control_visibility()) { 1856 return $returnactionmenulink ? null : ''; 1857 } 1858 1859 if (!has_capability('moodle/grade:manage', $this->context) and 1860 !has_capability('moodle/grade:hide', $this->context)) { 1861 return $returnactionmenulink ? null : ''; 1862 } 1863 1864 $strparams = $this->get_params_for_iconstr($element); 1865 $strshow = get_string('showverbose', 'grades', $strparams); 1866 $strhide = get_string('hideverbose', 'grades', $strparams); 1867 1868 $url = new moodle_url('/grade/edit/tree/action.php', array('id' => $this->courseid, 'sesskey' => sesskey(), 'eid' => $element['eid'])); 1869 $url = $gpr->add_url_params($url); 1870 1871 if ($element['object']->is_hidden()) { 1872 $type = 'show'; 1873 $tooltip = $strshow; 1874 1875 // Change the icon and add a tooltip showing the date 1876 if ($element['type'] != 'category' and $element['object']->get_hidden() > 1) { 1877 $type = 'hiddenuntil'; 1878 $tooltip = get_string('hiddenuntildate', 'grades', 1879 userdate($element['object']->get_hidden())); 1880 } 1881 1882 $url->param('action', 'show'); 1883 1884 if ($returnactionmenulink) { 1885 $hideicon = new action_menu_link_secondary($url, new pix_icon('t/'.$type, $tooltip), get_string('show')); 1886 } else { 1887 $hideicon = $OUTPUT->action_icon($url, new pix_icon('t/'.$type, $tooltip, 'moodle', array('alt'=>$strshow, 'class'=>'smallicon'))); 1888 } 1889 1890 } else { 1891 $url->param('action', 'hide'); 1892 if ($returnactionmenulink) { 1893 $hideicon = new action_menu_link_secondary($url, new pix_icon('t/hide', $strhide), get_string('hide')); 1894 } else { 1895 $hideicon = $OUTPUT->action_icon($url, new pix_icon('t/hide', $strhide)); 1896 } 1897 } 1898 1899 return $hideicon; 1900 } 1901 1902 /** 1903 * Return locking icon for given element 1904 * 1905 * @param array $element An array representing an element in the grade_tree 1906 * @param object $gpr A grade_plugin_return object 1907 * 1908 * @return string 1909 */ 1910 public function get_locking_icon($element, $gpr) { 1911 global $CFG, $OUTPUT; 1912 1913 $strparams = $this->get_params_for_iconstr($element); 1914 $strunlock = get_string('unlockverbose', 'grades', $strparams); 1915 $strlock = get_string('lockverbose', 'grades', $strparams); 1916 1917 $url = new moodle_url('/grade/edit/tree/action.php', array('id' => $this->courseid, 'sesskey' => sesskey(), 'eid' => $element['eid'])); 1918 $url = $gpr->add_url_params($url); 1919 1920 // Don't allow an unlocking action for a grade whose grade item is locked: just print a state icon 1921 if ($element['type'] == 'grade' && $element['object']->grade_item->is_locked()) { 1922 $strparamobj = new stdClass(); 1923 $strparamobj->itemname = $element['object']->grade_item->itemname; 1924 $strnonunlockable = get_string('nonunlockableverbose', 'grades', $strparamobj); 1925 1926 $action = html_writer::tag('span', $OUTPUT->pix_icon('t/locked', $strnonunlockable), 1927 array('class' => 'action-icon')); 1928 1929 } else if ($element['object']->is_locked()) { 1930 $type = 'unlock'; 1931 $tooltip = $strunlock; 1932 1933 // Change the icon and add a tooltip showing the date 1934 if ($element['type'] != 'category' and $element['object']->get_locktime() > 1) { 1935 $type = 'locktime'; 1936 $tooltip = get_string('locktimedate', 'grades', 1937 userdate($element['object']->get_locktime())); 1938 } 1939 1940 if (!has_capability('moodle/grade:manage', $this->context) and !has_capability('moodle/grade:unlock', $this->context)) { 1941 $action = ''; 1942 } else { 1943 $url->param('action', 'unlock'); 1944 $action = $OUTPUT->action_icon($url, new pix_icon('t/'.$type, $tooltip, 'moodle', array('alt'=>$strunlock, 'class'=>'smallicon'))); 1945 } 1946 1947 } else { 1948 if (!has_capability('moodle/grade:manage', $this->context) and !has_capability('moodle/grade:lock', $this->context)) { 1949 $action = ''; 1950 } else { 1951 $url->param('action', 'lock'); 1952 $action = $OUTPUT->action_icon($url, new pix_icon('t/lock', $strlock)); 1953 } 1954 } 1955 1956 return $action; 1957 } 1958 1959 /** 1960 * Return calculation icon for given element 1961 * 1962 * @param array $element An array representing an element in the grade_tree 1963 * @param object $gpr A grade_plugin_return object 1964 * @param bool $returnactionmenulink return the instance of action_menu_link instead of string 1965 * @return string|action_menu_link 1966 */ 1967 public function get_calculation_icon($element, $gpr, $returnactionmenulink = false) { 1968 global $CFG, $OUTPUT; 1969 if (!has_capability('moodle/grade:manage', $this->context)) { 1970 return $returnactionmenulink ? null : ''; 1971 } 1972 1973 $type = $element['type']; 1974 $object = $element['object']; 1975 1976 if ($type == 'item' or $type == 'courseitem' or $type == 'categoryitem') { 1977 $strparams = $this->get_params_for_iconstr($element); 1978 $streditcalculation = get_string('editcalculationverbose', 'grades', $strparams); 1979 1980 $is_scale = $object->gradetype == GRADE_TYPE_SCALE; 1981 $is_value = $object->gradetype == GRADE_TYPE_VALUE; 1982 1983 // show calculation icon only when calculation possible 1984 if (!$object->is_external_item() and ($is_scale or $is_value)) { 1985 if ($object->is_calculated()) { 1986 $icon = 't/calc'; 1987 } else { 1988 $icon = 't/calc_off'; 1989 } 1990 1991 $url = new moodle_url('/grade/edit/tree/calculation.php', array('courseid' => $this->courseid, 'id' => $object->id)); 1992 $url = $gpr->add_url_params($url); 1993 if ($returnactionmenulink) { 1994 return new action_menu_link_secondary($url, 1995 new pix_icon($icon, $streditcalculation), 1996 get_string('editcalculation', 'grades')); 1997 } else { 1998 return $OUTPUT->action_icon($url, new pix_icon($icon, $streditcalculation)); 1999 } 2000 } 2001 } 2002 2003 return $returnactionmenulink ? null : ''; 2004 } 2005 } 2006 2007 /** 2008 * Flat structure similar to grade tree. 2009 * 2010 * @uses grade_structure 2011 * @package core_grades 2012 * @copyright 2009 Nicolas Connault 2013 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 2014 */ 2015 class grade_seq extends grade_structure { 2016 2017 /** 2018 * 1D array of elements 2019 */ 2020 public $elements; 2021 2022 /** 2023 * Constructor, retrieves and stores array of all grade_category and grade_item 2024 * objects for the given courseid. Full objects are instantiated. Ordering sequence is fixed if needed. 2025 * 2026 * @param int $courseid The course id 2027 * @param bool $category_grade_last category grade item is the last child 2028 * @param bool $nooutcomes Whether or not outcomes should be included 2029 */ 2030 public function __construct($courseid, $category_grade_last=false, $nooutcomes=false) { 2031 global $USER, $CFG; 2032 2033 $this->courseid = $courseid; 2034 $this->context = context_course::instance($courseid); 2035 2036 // get course grade tree 2037 $top_element = grade_category::fetch_course_tree($courseid, true); 2038 2039 $this->elements = grade_seq::flatten($top_element, $category_grade_last, $nooutcomes); 2040 2041 foreach ($this->elements as $key=>$unused) { 2042 $this->items[$this->elements[$key]['object']->id] =& $this->elements[$key]['object']; 2043 } 2044 } 2045 2046 /** 2047 * Old syntax of class constructor. Deprecated in PHP7. 2048 * 2049 * @deprecated since Moodle 3.1 2050 */ 2051 public function grade_seq($courseid, $category_grade_last=false, $nooutcomes=false) { 2052 debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); 2053 self::__construct($courseid, $category_grade_last, $nooutcomes); 2054 } 2055 2056 /** 2057 * Static recursive helper - makes the grade_item for category the last children 2058 * 2059 * @param array &$element The seed of the recursion 2060 * @param bool $category_grade_last category grade item is the last child 2061 * @param bool $nooutcomes Whether or not outcomes should be included 2062 * 2063 * @return array 2064 */ 2065 public function flatten(&$element, $category_grade_last, $nooutcomes) { 2066 if (empty($element['children'])) { 2067 return array(); 2068 } 2069 $children = array(); 2070 2071 foreach ($element['children'] as $sortorder=>$unused) { 2072 if ($nooutcomes and $element['type'] != 'category' and 2073 $element['children'][$sortorder]['object']->is_outcome_item()) { 2074 continue; 2075 } 2076 $children[] = $element['children'][$sortorder]; 2077 } 2078 unset($element['children']); 2079 2080 if ($category_grade_last and count($children) > 1 and 2081 ( 2082 $children[0]['type'] === 'courseitem' or 2083 $children[0]['type'] === 'categoryitem' 2084 ) 2085 ) { 2086 $cat_item = array_shift($children); 2087 array_push($children, $cat_item); 2088 } 2089 2090 $result = array(); 2091 foreach ($children as $child) { 2092 if ($child['type'] == 'category') { 2093 $result = $result + grade_seq::flatten($child, $category_grade_last, $nooutcomes); 2094 } else { 2095 $child['eid'] = 'i'.$child['object']->id; 2096 $result[$child['object']->id] = $child; 2097 } 2098 } 2099 2100 return $result; 2101 } 2102 2103 /** 2104 * Parses the array in search of a given eid and returns a element object with 2105 * information about the element it has found. 2106 * 2107 * @param int $eid Gradetree Element ID 2108 * 2109 * @return object element 2110 */ 2111 public function locate_element($eid) { 2112 // it is a grade - construct a new object 2113 if (strpos($eid, 'n') === 0) { 2114 if (!preg_match('/n(\d+)u(\d+)/', $eid, $matches)) { 2115 return null; 2116 } 2117 2118 $itemid = $matches[1]; 2119 $userid = $matches[2]; 2120 2121 //extra security check - the grade item must be in this tree 2122 if (!$item_el = $this->locate_element('ig'.$itemid)) { 2123 return null; 2124 } 2125 2126 // $gradea->id may be null - means does not exist yet 2127 $grade = new grade_grade(array('itemid'=>$itemid, 'userid'=>$userid)); 2128 2129 $grade->grade_item =& $item_el['object']; // this may speedup grade_grade methods! 2130 return array('eid'=>'n'.$itemid.'u'.$userid,'object'=>$grade, 'type'=>'grade'); 2131 2132 } else if (strpos($eid, 'g') === 0) { 2133 $id = (int) substr($eid, 1); 2134 if (!$grade = grade_grade::fetch(array('id'=>$id))) { 2135 return null; 2136 } 2137 //extra security check - the grade item must be in this tree 2138 if (!$item_el = $this->locate_element('ig'.$grade->itemid)) { 2139 return null; 2140 } 2141 $grade->grade_item =& $item_el['object']; // this may speedup grade_grade methods! 2142 return array('eid'=>'g'.$id,'object'=>$grade, 'type'=>'grade'); 2143 } 2144 2145 // it is a category or item 2146 foreach ($this->elements as $element) { 2147 if ($element['eid'] == $eid) { 2148 return $element; 2149 } 2150 } 2151 2152 return null; 2153 } 2154 } 2155 2156 /** 2157 * This class represents a complete tree of categories, grade_items and final grades, 2158 * organises as an array primarily, but which can also be converted to other formats. 2159 * It has simple method calls with complex implementations, allowing for easy insertion, 2160 * deletion and moving of items and categories within the tree. 2161 * 2162 * @uses grade_structure 2163 * @package core_grades 2164 * @copyright 2009 Nicolas Connault 2165 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 2166 */ 2167 class grade_tree extends grade_structure { 2168 2169 /** 2170 * The basic representation of the tree as a hierarchical, 3-tiered array. 2171 * @var object $top_element 2172 */ 2173 public $top_element; 2174 2175 /** 2176 * 2D array of grade items and categories 2177 * @var array $levels 2178 */ 2179 public $levels; 2180 2181 /** 2182 * Grade items 2183 * @var array $items 2184 */ 2185 public $items; 2186 2187 /** 2188 * Constructor, retrieves and stores a hierarchical array of all grade_category and grade_item 2189 * objects for the given courseid. Full objects are instantiated. Ordering sequence is fixed if needed. 2190 * 2191 * @param int $courseid The Course ID 2192 * @param bool $fillers include fillers and colspans, make the levels var "rectangular" 2193 * @param bool $category_grade_last category grade item is the last child 2194 * @param array $collapsed array of collapsed categories 2195 * @param bool $nooutcomes Whether or not outcomes should be included 2196 */ 2197 public function __construct($courseid, $fillers=true, $category_grade_last=false, 2198 $collapsed=null, $nooutcomes=false) { 2199 global $USER, $CFG, $COURSE, $DB; 2200 2201 $this->courseid = $courseid; 2202 $this->levels = array(); 2203 $this->context = context_course::instance($courseid); 2204 2205 if (!empty($COURSE->id) && $COURSE->id == $this->courseid) { 2206 $course = $COURSE; 2207 } else { 2208 $course = $DB->get_record('course', array('id' => $this->courseid)); 2209 } 2210 $this->modinfo = get_fast_modinfo($course); 2211 2212 // get course grade tree 2213 $this->top_element = grade_category::fetch_course_tree($courseid, true); 2214 2215 // collapse the categories if requested 2216 if (!empty($collapsed)) { 2217 grade_tree::category_collapse($this->top_element, $collapsed); 2218 } 2219 2220 // no otucomes if requested 2221 if (!empty($nooutcomes)) { 2222 grade_tree::no_outcomes($this->top_element); 2223 } 2224 2225 // move category item to last position in category 2226 if ($category_grade_last) { 2227 grade_tree::category_grade_last($this->top_element); 2228 } 2229 2230 if ($fillers) { 2231 // inject fake categories == fillers 2232 grade_tree::inject_fillers($this->top_element, 0); 2233 // add colspans to categories and fillers 2234 grade_tree::inject_colspans($this->top_element); 2235 } 2236 2237 grade_tree::fill_levels($this->levels, $this->top_element, 0); 2238 2239 } 2240 2241 /** 2242 * Old syntax of class constructor. Deprecated in PHP7. 2243 * 2244 * @deprecated since Moodle 3.1 2245 */ 2246 public function grade_tree($courseid, $fillers=true, $category_grade_last=false, 2247 $collapsed=null, $nooutcomes=false) { 2248 debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); 2249 self::__construct($courseid, $fillers, $category_grade_last, $collapsed, $nooutcomes); 2250 } 2251 2252 /** 2253 * Static recursive helper - removes items from collapsed categories 2254 * 2255 * @param array &$element The seed of the recursion 2256 * @param array $collapsed array of collapsed categories 2257 * 2258 * @return void 2259 */ 2260 public function category_collapse(&$element, $collapsed) { 2261 if ($element['type'] != 'category') { 2262 return; 2263 } 2264 if (empty($element['children']) or count($element['children']) < 2) { 2265 return; 2266 } 2267 2268 if (in_array($element['object']->id, $collapsed['aggregatesonly'])) { 2269 $category_item = reset($element['children']); //keep only category item 2270 $element['children'] = array(key($element['children'])=>$category_item); 2271 2272 } else { 2273 if (in_array($element['object']->id, $collapsed['gradesonly'])) { // Remove category item 2274 reset($element['children']); 2275 $first_key = key($element['children']); 2276 unset($element['children'][$first_key]); 2277 } 2278 foreach ($element['children'] as $sortorder=>$child) { // Recurse through the element's children 2279 grade_tree::category_collapse($element['children'][$sortorder], $collapsed); 2280 } 2281 } 2282 } 2283 2284 /** 2285 * Static recursive helper - removes all outcomes 2286 * 2287 * @param array &$element The seed of the recursion 2288 * 2289 * @return void 2290 */ 2291 public function no_outcomes(&$element) { 2292 if ($element['type'] != 'category') { 2293 return; 2294 } 2295 foreach ($element['children'] as $sortorder=>$child) { 2296 if ($element['children'][$sortorder]['type'] == 'item' 2297 and $element['children'][$sortorder]['object']->is_outcome_item()) { 2298 unset($element['children'][$sortorder]); 2299 2300 } else if ($element['children'][$sortorder]['type'] == 'category') { 2301 grade_tree::no_outcomes($element['children'][$sortorder]); 2302 } 2303 } 2304 } 2305 2306 /** 2307 * Static recursive helper - makes the grade_item for category the last children 2308 * 2309 * @param array &$element The seed of the recursion 2310 * 2311 * @return void 2312 */ 2313 public function category_grade_last(&$element) { 2314 if (empty($element['children'])) { 2315 return; 2316 } 2317 if (count($element['children']) < 2) { 2318 return; 2319 } 2320 $first_item = reset($element['children']); 2321 if ($first_item['type'] == 'categoryitem' or $first_item['type'] == 'courseitem') { 2322 // the category item might have been already removed 2323 $order = key($element['children']); 2324 unset($element['children'][$order]); 2325 $element['children'][$order] =& $first_item; 2326 } 2327 foreach ($element['children'] as $sortorder => $child) { 2328 grade_tree::category_grade_last($element['children'][$sortorder]); 2329 } 2330 } 2331 2332 /** 2333 * Static recursive helper - fills the levels array, useful when accessing tree elements of one level 2334 * 2335 * @param array &$levels The levels of the grade tree through which to recurse 2336 * @param array &$element The seed of the recursion 2337 * @param int $depth How deep are we? 2338 * @return void 2339 */ 2340 public function fill_levels(&$levels, &$element, $depth) { 2341 if (!array_key_exists($depth, $levels)) { 2342 $levels[$depth] = array(); 2343 } 2344 2345 // prepare unique identifier 2346 if ($element['type'] == 'category') { 2347 $element['eid'] = 'cg'.$element['object']->id; 2348 } else if (in_array($element['type'], array('item', 'courseitem', 'categoryitem'))) { 2349 $element['eid'] = 'ig'.$element['object']->id; 2350 $this->items[$element['object']->id] =& $element['object']; 2351 } 2352 2353 $levels[$depth][] =& $element; 2354 $depth++; 2355 if (empty($element['children'])) { 2356 return; 2357 } 2358 $prev = 0; 2359 foreach ($element['children'] as $sortorder=>$child) { 2360 grade_tree::fill_levels($levels, $element['children'][$sortorder], $depth); 2361 $element['children'][$sortorder]['prev'] = $prev; 2362 $element['children'][$sortorder]['next'] = 0; 2363 if ($prev) { 2364 $element['children'][$prev]['next'] = $sortorder; 2365 } 2366 $prev = $sortorder; 2367 } 2368 } 2369 2370 /** 2371 * Determines whether the grade tree item can be displayed. 2372 * This is particularly targeted for grade categories that have no total (None) when rendering the grade tree. 2373 * It checks if the grade tree item is of type 'category', and makes sure that the category, or at least one of children, 2374 * can be output. 2375 * 2376 * @param array $element The grade category element. 2377 * @return bool True if the grade tree item can be displayed. False, otherwise. 2378 */ 2379 public static function can_output_item($element) { 2380 $canoutput = true; 2381 2382 if ($element['type'] === 'category') { 2383 $object = $element['object']; 2384 $category = grade_category::fetch(array('id' => $object->id)); 2385 // Category has total, we can output this. 2386 if ($category->get_grade_item()->gradetype != GRADE_TYPE_NONE) { 2387 return true; 2388 } 2389 2390 // Category has no total and has no children, no need to output this. 2391 if (empty($element['children'])) { 2392 return false; 2393 } 2394 2395 $canoutput = false; 2396 // Loop over children and make sure at least one child can be output. 2397 foreach ($element['children'] as $child) { 2398 $canoutput = self::can_output_item($child); 2399 if ($canoutput) { 2400 break; 2401 } 2402 } 2403 } 2404 2405 return $canoutput; 2406 } 2407 2408 /** 2409 * Static recursive helper - makes full tree (all leafes are at the same level) 2410 * 2411 * @param array &$element The seed of the recursion 2412 * @param int $depth How deep are we? 2413 * 2414 * @return int 2415 */ 2416 public function inject_fillers(&$element, $depth) { 2417 $depth++; 2418 2419 if (empty($element['children'])) { 2420 return $depth; 2421 } 2422 $chdepths = array(); 2423 $chids = array_keys($element['children']); 2424 $last_child = end($chids); 2425 $first_child = reset($chids); 2426 2427 foreach ($chids as $chid) { 2428 $chdepths[$chid] = grade_tree::inject_fillers($element['children'][$chid], $depth); 2429 } 2430 arsort($chdepths); 2431 2432 $maxdepth = reset($chdepths); 2433 foreach ($chdepths as $chid=>$chd) { 2434 if ($chd == $maxdepth) { 2435 continue; 2436 } 2437 if (!self::can_output_item($element['children'][$chid])) { 2438 continue; 2439 } 2440 for ($i=0; $i < $maxdepth-$chd; $i++) { 2441 if ($chid == $first_child) { 2442 $type = 'fillerfirst'; 2443 } else if ($chid == $last_child) { 2444 $type = 'fillerlast'; 2445 } else { 2446 $type = 'filler'; 2447 } 2448 $oldchild =& $element['children'][$chid]; 2449 $element['children'][$chid] = array('object'=>'filler', 'type'=>$type, 2450 'eid'=>'', 'depth'=>$element['object']->depth, 2451 'children'=>array($oldchild)); 2452 } 2453 } 2454 2455 return $maxdepth; 2456 } 2457 2458 /** 2459 * Static recursive helper - add colspan information into categories 2460 * 2461 * @param array &$element The seed of the recursion 2462 * 2463 * @return int 2464 */ 2465 public function inject_colspans(&$element) { 2466 if (empty($element['children'])) { 2467 return 1; 2468 } 2469 $count = 0; 2470 foreach ($element['children'] as $key=>$child) { 2471 if (!self::can_output_item($child)) { 2472 continue; 2473 } 2474 $count += grade_tree::inject_colspans($element['children'][$key]); 2475 } 2476 $element['colspan'] = $count; 2477 return $count; 2478 } 2479 2480 /** 2481 * Parses the array in search of a given eid and returns a element object with 2482 * information about the element it has found. 2483 * @param int $eid Gradetree Element ID 2484 * @return object element 2485 */ 2486 public function locate_element($eid) { 2487 // it is a grade - construct a new object 2488 if (strpos($eid, 'n') === 0) { 2489 if (!preg_match('/n(\d+)u(\d+)/', $eid, $matches)) { 2490 return null; 2491 } 2492 2493 $itemid = $matches[1]; 2494 $userid = $matches[2]; 2495 2496 //extra security check - the grade item must be in this tree 2497 if (!$item_el = $this->locate_element('ig'.$itemid)) { 2498 return null; 2499 } 2500 2501 // $gradea->id may be null - means does not exist yet 2502 $grade = new grade_grade(array('itemid'=>$itemid, 'userid'=>$userid)); 2503 2504 $grade->grade_item =& $item_el['object']; // this may speedup grade_grade methods! 2505 return array('eid'=>'n'.$itemid.'u'.$userid,'object'=>$grade, 'type'=>'grade'); 2506 2507 } else if (strpos($eid, 'g') === 0) { 2508 $id = (int) substr($eid, 1); 2509 if (!$grade = grade_grade::fetch(array('id'=>$id))) { 2510 return null; 2511 } 2512 //extra security check - the grade item must be in this tree 2513 if (!$item_el = $this->locate_element('ig'.$grade->itemid)) { 2514 return null; 2515 } 2516 $grade->grade_item =& $item_el['object']; // this may speedup grade_grade methods! 2517 return array('eid'=>'g'.$id,'object'=>$grade, 'type'=>'grade'); 2518 } 2519 2520 // it is a category or item 2521 foreach ($this->levels as $row) { 2522 foreach ($row as $element) { 2523 if ($element['type'] == 'filler') { 2524 continue; 2525 } 2526 if ($element['eid'] == $eid) { 2527 return $element; 2528 } 2529 } 2530 } 2531 2532 return null; 2533 } 2534 2535 /** 2536 * Returns a well-formed XML representation of the grade-tree using recursion. 2537 * 2538 * @param array $root The current element in the recursion. If null, starts at the top of the tree. 2539 * @param string $tabs The control character to use for tabs 2540 * 2541 * @return string $xml 2542 */ 2543 public function exporttoxml($root=null, $tabs="\t") { 2544 $xml = null; 2545 $first = false; 2546 if (is_null($root)) { 2547 $root = $this->top_element; 2548 $xml = '<?xml version="1.0" encoding="UTF-8" ?>' . "\n"; 2549 $xml .= "<gradetree>\n"; 2550 $first = true; 2551 } 2552 2553 $type = 'undefined'; 2554 if (strpos($root['object']->table, 'grade_categories') !== false) { 2555 $type = 'category'; 2556 } else if (strpos($root['object']->table, 'grade_items') !== false) { 2557 $type = 'item'; 2558 } else if (strpos($root['object']->table, 'grade_outcomes') !== false) { 2559 $type = 'outcome'; 2560 } 2561 2562 $xml .= "$tabs<element type=\"$type\">\n"; 2563 foreach ($root['object'] as $var => $value) { 2564 if (!is_object($value) && !is_array($value) && !empty($value)) { 2565 $xml .= "$tabs\t<$var>$value</$var>\n"; 2566 } 2567 } 2568 2569 if (!empty($root['children'])) { 2570 $xml .= "$tabs\t<children>\n"; 2571 foreach ($root['children'] as $sortorder => $child) { 2572 $xml .= $this->exportToXML($child, $tabs."\t\t"); 2573 } 2574 $xml .= "$tabs\t</children>\n"; 2575 } 2576 2577 $xml .= "$tabs</element>\n"; 2578 2579 if ($first) { 2580 $xml .= "</gradetree>"; 2581 } 2582 2583 return $xml; 2584 } 2585 2586 /** 2587 * Returns a JSON representation of the grade-tree using recursion. 2588 * 2589 * @param array $root The current element in the recursion. If null, starts at the top of the tree. 2590 * @param string $tabs Tab characters used to indent the string nicely for humans to enjoy 2591 * 2592 * @return string 2593 */ 2594 public function exporttojson($root=null, $tabs="\t") { 2595 $json = null; 2596 $first = false; 2597 if (is_null($root)) { 2598 $root = $this->top_element; 2599 $first = true; 2600 } 2601 2602 $name = ''; 2603 2604 2605 if (strpos($root['object']->table, 'grade_categories') !== false) { 2606 $name = $root['object']->fullname; 2607 if ($name == '?') { 2608 $name = $root['object']->get_name(); 2609 } 2610 } else if (strpos($root['object']->table, 'grade_items') !== false) { 2611 $name = $root['object']->itemname; 2612 } else if (strpos($root['object']->table, 'grade_outcomes') !== false) { 2613 $name = $root['object']->itemname; 2614 } 2615 2616 $json .= "$tabs {\n"; 2617 $json .= "$tabs\t \"type\": \"{$root['type']}\",\n"; 2618 $json .= "$tabs\t \"name\": \"$name\",\n"; 2619 2620 foreach ($root['object'] as $var => $value) { 2621 if (!is_object($value) && !is_array($value) && !empty($value)) { 2622 $json .= "$tabs\t \"$var\": \"$value\",\n"; 2623 } 2624 } 2625 2626 $json = substr($json, 0, strrpos($json, ',')); 2627 2628 if (!empty($root['children'])) { 2629 $json .= ",\n$tabs\t\"children\": [\n"; 2630 foreach ($root['children'] as $sortorder => $child) { 2631 $json .= $this->exportToJSON($child, $tabs."\t\t"); 2632 } 2633 $json = substr($json, 0, strrpos($json, ',')); 2634 $json .= "\n$tabs\t]\n"; 2635 } 2636 2637 if ($first) { 2638 $json .= "\n}"; 2639 } else { 2640 $json .= "\n$tabs},\n"; 2641 } 2642 2643 return $json; 2644 } 2645 2646 /** 2647 * Returns the array of levels 2648 * 2649 * @return array 2650 */ 2651 public function get_levels() { 2652 return $this->levels; 2653 } 2654 2655 /** 2656 * Returns the array of grade items 2657 * 2658 * @return array 2659 */ 2660 public function get_items() { 2661 return $this->items; 2662 } 2663 2664 /** 2665 * Returns a specific Grade Item 2666 * 2667 * @param int $itemid The ID of the grade_item object 2668 * 2669 * @return grade_item 2670 */ 2671 public function get_item($itemid) { 2672 if (array_key_exists($itemid, $this->items)) { 2673 return $this->items[$itemid]; 2674 } else { 2675 return false; 2676 } 2677 } 2678 } 2679 2680 /** 2681 * Local shortcut function for creating an edit/delete button for a grade_* object. 2682 * @param string $type 'edit' or 'delete' 2683 * @param int $courseid The Course ID 2684 * @param grade_* $object The grade_* object 2685 * @return string html 2686 */ 2687 function grade_button($type, $courseid, $object) { 2688 global $CFG, $OUTPUT; 2689 if (preg_match('/grade_(.*)/', get_class($object), $matches)) { 2690 $objectidstring = $matches[1] . 'id'; 2691 } else { 2692 throw new coding_exception('grade_button() only accepts grade_* objects as third parameter!'); 2693 } 2694 2695 $strdelete = get_string('delete'); 2696 $stredit = get_string('edit'); 2697 2698 if ($type == 'delete') { 2699 $url = new moodle_url('index.php', array('id' => $courseid, $objectidstring => $object->id, 'action' => 'delete', 'sesskey' => sesskey())); 2700 } else if ($type == 'edit') { 2701 $url = new moodle_url('edit.php', array('courseid' => $courseid, 'id' => $object->id)); 2702 } 2703 2704 return $OUTPUT->action_icon($url, new pix_icon('t/'.$type, ${'str'.$type}, '', array('class' => 'iconsmall'))); 2705 2706 } 2707 2708 /** 2709 * This method adds settings to the settings block for the grade system and its 2710 * plugins 2711 * 2712 * @global moodle_page $PAGE 2713 */ 2714 function grade_extend_settings($plugininfo, $courseid) { 2715 global $PAGE; 2716 2717 $gradenode = $PAGE->settingsnav->prepend(get_string('gradeadministration', 'grades'), null, navigation_node::TYPE_CONTAINER, 2718 null, 'gradeadmin'); 2719 2720 $strings = array_shift($plugininfo); 2721 2722 if ($reports = grade_helper::get_plugins_reports($courseid)) { 2723 foreach ($reports as $report) { 2724 $gradenode->add($report->string, $report->link, navigation_node::TYPE_SETTING, null, $report->id, new pix_icon('i/report', '')); 2725 } 2726 } 2727 2728 if ($settings = grade_helper::get_info_manage_settings($courseid)) { 2729 $settingsnode = $gradenode->add($strings['settings'], null, navigation_node::TYPE_CONTAINER); 2730 foreach ($settings as $setting) { 2731 $settingsnode->add($setting->string, $setting->link, navigation_node::TYPE_SETTING, null, $setting->id, new pix_icon('i/settings', '')); 2732 } 2733 } 2734 2735 if ($imports = grade_helper::get_plugins_import($courseid)) { 2736 $importnode = $gradenode->add($strings['import'], null, navigation_node::TYPE_CONTAINER); 2737 foreach ($imports as $import) { 2738 $importnode->add($import->string, $import->link, navigation_node::TYPE_SETTING, null, $import->id, new pix_icon('i/import', '')); 2739 } 2740 } 2741 2742 if ($exports = grade_helper::get_plugins_export($courseid)) { 2743 $exportnode = $gradenode->add($strings['export'], null, navigation_node::TYPE_CONTAINER); 2744 foreach ($exports as $export) { 2745 $exportnode->add($export->string, $export->link, navigation_node::TYPE_SETTING, null, $export->id, new pix_icon('i/export', '')); 2746 } 2747 } 2748 2749 if ($letters = grade_helper::get_info_letters($courseid)) { 2750 $letters = array_shift($letters); 2751 $gradenode->add($strings['letter'], $letters->link, navigation_node::TYPE_SETTING, null, $letters->id, new pix_icon('i/settings', '')); 2752 } 2753 2754 if ($outcomes = grade_helper::get_info_outcomes($courseid)) { 2755 $outcomes = array_shift($outcomes); 2756 $gradenode->add($strings['outcome'], $outcomes->link, navigation_node::TYPE_SETTING, null, $outcomes->id, new pix_icon('i/outcomes', '')); 2757 } 2758 2759 if ($scales = grade_helper::get_info_scales($courseid)) { 2760 $gradenode->add($strings['scale'], $scales->link, navigation_node::TYPE_SETTING, null, $scales->id, new pix_icon('i/scales', '')); 2761 } 2762 2763 if ($gradenode->contains_active_node()) { 2764 // If the gradenode is active include the settings base node (gradeadministration) in 2765 // the navbar, typcially this is ignored. 2766 $PAGE->navbar->includesettingsbase = true; 2767 2768 // If we can get the course admin node make sure it is closed by default 2769 // as in this case the gradenode will be opened 2770 if ($coursenode = $PAGE->settingsnav->get('courseadmin', navigation_node::TYPE_COURSE)){ 2771 $coursenode->make_inactive(); 2772 $coursenode->forceopen = false; 2773 } 2774 } 2775 } 2776 2777 /** 2778 * Grade helper class 2779 * 2780 * This class provides several helpful functions that work irrespective of any 2781 * current state. 2782 * 2783 * @copyright 2010 Sam Hemelryk 2784 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 2785 */ 2786 abstract class grade_helper { 2787 /** 2788 * Cached manage settings info {@see get_info_settings} 2789 * @var grade_plugin_info|false 2790 */ 2791 protected static $managesetting = null; 2792 /** 2793 * Cached grade report plugins {@see get_plugins_reports} 2794 * @var array|false 2795 */ 2796 protected static $gradereports = null; 2797 /** 2798 * Cached grade report plugins preferences {@see get_info_scales} 2799 * @var array|false 2800 */ 2801 protected static $gradereportpreferences = null; 2802 /** 2803 * Cached scale info {@see get_info_scales} 2804 * @var grade_plugin_info|false 2805 */ 2806 protected static $scaleinfo = null; 2807 /** 2808 * Cached outcome info {@see get_info_outcomes} 2809 * @var grade_plugin_info|false 2810 */ 2811 protected static $outcomeinfo = null; 2812 /** 2813 * Cached leftter info {@see get_info_letters} 2814 * @var grade_plugin_info|false 2815 */ 2816 protected static $letterinfo = null; 2817 /** 2818 * Cached grade import plugins {@see get_plugins_import} 2819 * @var array|false 2820 */ 2821 protected static $importplugins = null; 2822 /** 2823 * Cached grade export plugins {@see get_plugins_export} 2824 * @var array|false 2825 */ 2826 protected static $exportplugins = null; 2827 /** 2828 * Cached grade plugin strings 2829 * @var array 2830 */ 2831 protected static $pluginstrings = null; 2832 /** 2833 * Cached grade aggregation strings 2834 * @var array 2835 */ 2836 protected static $aggregationstrings = null; 2837 2838 /** 2839 * Gets strings commonly used by the describe plugins 2840 * 2841 * report => get_string('view'), 2842 * scale => get_string('scales'), 2843 * outcome => get_string('outcomes', 'grades'), 2844 * letter => get_string('letters', 'grades'), 2845 * export => get_string('export', 'grades'), 2846 * import => get_string('import'), 2847 * settings => get_string('settings') 2848 * 2849 * @return array 2850 */ 2851 public static function get_plugin_strings() { 2852 if (self::$pluginstrings === null) { 2853 self::$pluginstrings = array( 2854 'report' => get_string('view'), 2855 'scale' => get_string('scales'), 2856 'outcome' => get_string('outcomes', 'grades'), 2857 'letter' => get_string('letters', 'grades'), 2858 'export' => get_string('export', 'grades'), 2859 'import' => get_string('import'), 2860 'settings' => get_string('edittree', 'grades') 2861 ); 2862 } 2863 return self::$pluginstrings; 2864 } 2865 2866 /** 2867 * Gets strings describing the available aggregation methods. 2868 * 2869 * @return array 2870 */ 2871 public static function get_aggregation_strings() { 2872 if (self::$aggregationstrings === null) { 2873 self::$aggregationstrings = array( 2874 GRADE_AGGREGATE_MEAN => get_string('aggregatemean', 'grades'), 2875 GRADE_AGGREGATE_WEIGHTED_MEAN => get_string('aggregateweightedmean', 'grades'), 2876 GRADE_AGGREGATE_WEIGHTED_MEAN2 => get_string('aggregateweightedmean2', 'grades'), 2877 GRADE_AGGREGATE_EXTRACREDIT_MEAN => get_string('aggregateextracreditmean', 'grades'), 2878 GRADE_AGGREGATE_MEDIAN => get_string('aggregatemedian', 'grades'), 2879 GRADE_AGGREGATE_MIN => get_string('aggregatemin', 'grades'), 2880 GRADE_AGGREGATE_MAX => get_string('aggregatemax', 'grades'), 2881 GRADE_AGGREGATE_MODE => get_string('aggregatemode', 'grades'), 2882 GRADE_AGGREGATE_SUM => get_string('aggregatesum', 'grades') 2883 ); 2884 } 2885 return self::$aggregationstrings; 2886 } 2887 2888 /** 2889 * Get grade_plugin_info object for managing settings if the user can 2890 * 2891 * @param int $courseid 2892 * @return grade_plugin_info[] 2893 */ 2894 public static function get_info_manage_settings($courseid) { 2895 if (self::$managesetting !== null) { 2896 return self::$managesetting; 2897 } 2898 $context = context_course::instance($courseid); 2899 self::$managesetting = array(); 2900 if ($courseid != SITEID && has_capability('moodle/grade:manage', $context)) { 2901 self::$managesetting['gradebooksetup'] = new grade_plugin_info('setup', 2902 new moodle_url('/grade/edit/tree/index.php', array('id' => $courseid)), 2903 get_string('gradebooksetup', 'grades')); 2904 self::$managesetting['coursesettings'] = new grade_plugin_info('coursesettings', 2905 new moodle_url('/grade/edit/settings/index.php', array('id'=>$courseid)), 2906 get_string('coursegradesettings', 'grades')); 2907 } 2908 if (self::$gradereportpreferences === null) { 2909 self::get_plugins_reports($courseid); 2910 } 2911 if (self::$gradereportpreferences) { 2912 self::$managesetting = array_merge(self::$managesetting, self::$gradereportpreferences); 2913 } 2914 return self::$managesetting; 2915 } 2916 /** 2917 * Returns an array of plugin reports as grade_plugin_info objects 2918 * 2919 * @param int $courseid 2920 * @return array 2921 */ 2922 public static function get_plugins_reports($courseid) { 2923 global $SITE, $CFG; 2924 2925 if (self::$gradereports !== null) { 2926 return self::$gradereports; 2927 } 2928 $context = context_course::instance($courseid); 2929 $gradereports = array(); 2930 $gradepreferences = array(); 2931 foreach (core_component::get_plugin_list('gradereport') as $plugin => $plugindir) { 2932 //some reports make no sense if we're not within a course 2933 if ($courseid==$SITE->id && ($plugin=='grader' || $plugin=='user')) { 2934 continue; 2935 } 2936 2937 // Remove outcomes report if outcomes not enabled. 2938 if ($plugin === 'outcomes' && empty($CFG->enableoutcomes)) { 2939 continue; 2940 } 2941 2942 // Remove ones we can't see 2943 if (!has_capability('gradereport/'.$plugin.':view', $context)) { 2944 continue; 2945 } 2946 2947 // Singleview doesn't doesn't accomodate for all cap combos yet, so this is hardcoded.. 2948 if ($plugin === 'singleview' && !has_all_capabilities(array('moodle/grade:viewall', 2949 'moodle/grade:edit'), $context)) { 2950 continue; 2951 } 2952 2953 $pluginstr = get_string('pluginname', 'gradereport_'.$plugin); 2954 $url = new moodle_url('/grade/report/'.$plugin.'/index.php', array('id'=>$courseid)); 2955 $gradereports[$plugin] = new grade_plugin_info($plugin, $url, $pluginstr); 2956 2957 // Add link to preferences tab if such a page exists 2958 if (file_exists($plugindir.'/preferences.php')) { 2959 $url = new moodle_url('/grade/report/'.$plugin.'/preferences.php', array('id'=>$courseid)); 2960 $gradepreferences[$plugin] = new grade_plugin_info($plugin, $url, 2961 get_string('preferences', 'grades') . ': ' . $pluginstr); 2962 } 2963 } 2964 if (count($gradereports) == 0) { 2965 $gradereports = false; 2966 $gradepreferences = false; 2967 } else if (count($gradepreferences) == 0) { 2968 $gradepreferences = false; 2969 asort($gradereports); 2970 } else { 2971 asort($gradereports); 2972 asort($gradepreferences); 2973 } 2974 self::$gradereports = $gradereports; 2975 self::$gradereportpreferences = $gradepreferences; 2976 return self::$gradereports; 2977 } 2978 2979 /** 2980 * Get information on scales 2981 * @param int $courseid 2982 * @return grade_plugin_info 2983 */ 2984 public static function get_info_scales($courseid) { 2985 if (self::$scaleinfo !== null) { 2986 return self::$scaleinfo; 2987 } 2988 if (has_capability('moodle/course:managescales', context_course::instance($courseid))) { 2989 $url = new moodle_url('/grade/edit/scale/index.php', array('id'=>$courseid)); 2990 self::$scaleinfo = new grade_plugin_info('scale', $url, get_string('view')); 2991 } else { 2992 self::$scaleinfo = false; 2993 } 2994 return self::$scaleinfo; 2995 } 2996 /** 2997 * Get information on outcomes 2998 * @param int $courseid 2999 * @return grade_plugin_info 3000 */ 3001 public static function get_info_outcomes($courseid) { 3002 global $CFG, $SITE; 3003 3004 if (self::$outcomeinfo !== null) { 3005 return self::$outcomeinfo; 3006 } 3007 $context = context_course::instance($courseid); 3008 $canmanage = has_capability('moodle/grade:manage', $context); 3009 $canupdate = has_capability('moodle/course:update', $context); 3010 if (!empty($CFG->enableoutcomes) && ($canmanage || $canupdate)) { 3011 $outcomes = array(); 3012 if ($canupdate) { 3013 if ($courseid!=$SITE->id) { 3014 $url = new moodle_url('/grade/edit/outcome/course.php', array('id'=>$courseid)); 3015 $outcomes['course'] = new grade_plugin_info('course', $url, get_string('outcomescourse', 'grades')); 3016 } 3017 $url = new moodle_url('/grade/edit/outcome/index.php', array('id'=>$courseid)); 3018 $outcomes['edit'] = new grade_plugin_info('edit', $url, get_string('editoutcomes', 'grades')); 3019 $url = new moodle_url('/grade/edit/outcome/import.php', array('courseid'=>$courseid)); 3020 $outcomes['import'] = new grade_plugin_info('import', $url, get_string('importoutcomes', 'grades')); 3021 } else { 3022 if ($courseid!=$SITE->id) { 3023 $url = new moodle_url('/grade/edit/outcome/course.php', array('id'=>$courseid)); 3024 $outcomes['edit'] = new grade_plugin_info('edit', $url, get_string('outcomescourse', 'grades')); 3025 } 3026 } 3027 self::$outcomeinfo = $outcomes; 3028 } else { 3029 self::$outcomeinfo = false; 3030 } 3031 return self::$outcomeinfo; 3032 } 3033 /** 3034 * Get information on letters 3035 * @param int $courseid 3036 * @return array 3037 */ 3038 public static function get_info_letters($courseid) { 3039 global $SITE; 3040 if (self::$letterinfo !== null) { 3041 return self::$letterinfo; 3042 } 3043 $context = context_course::instance($courseid); 3044 $canmanage = has_capability('moodle/grade:manage', $context); 3045 $canmanageletters = has_capability('moodle/grade:manageletters', $context); 3046 if ($canmanage || $canmanageletters) { 3047 // Redirect to system context when report is accessed from admin settings MDL-31633 3048 if ($context->instanceid == $SITE->id) { 3049 $param = array('edit' => 1); 3050 } else { 3051 $param = array('edit' => 1,'id' => $context->id); 3052 } 3053 self::$letterinfo = array( 3054 'view' => new grade_plugin_info('view', new moodle_url('/grade/edit/letter/index.php', array('id'=>$context->id)), get_string('view')), 3055 'edit' => new grade_plugin_info('edit', new moodle_url('/grade/edit/letter/index.php', $param), get_string('edit')) 3056 ); 3057 } else { 3058 self::$letterinfo = false; 3059 } 3060 return self::$letterinfo; 3061 } 3062 /** 3063 * Get information import plugins 3064 * @param int $courseid 3065 * @return array 3066 */ 3067 public static function get_plugins_import($courseid) { 3068 global $CFG; 3069 3070 if (self::$importplugins !== null) { 3071 return self::$importplugins; 3072 } 3073 $importplugins = array(); 3074 $context = context_course::instance($courseid); 3075 3076 if (has_capability('moodle/grade:import', $context)) { 3077 foreach (core_component::get_plugin_list('gradeimport') as $plugin => $plugindir) { 3078 if (!has_capability('gradeimport/'.$plugin.':view', $context)) { 3079 continue; 3080 } 3081 $pluginstr = get_string('pluginname', 'gradeimport_'.$plugin); 3082 $url = new moodle_url('/grade/import/'.$plugin.'/index.php', array('id'=>$courseid)); 3083 $importplugins[$plugin] = new grade_plugin_info($plugin, $url, $pluginstr); 3084 } 3085 3086 // Show key manager if grade publishing is enabled and the user has xml publishing capability. 3087 // XML is the only grade import plugin that has publishing feature. 3088 if ($CFG->gradepublishing && has_capability('gradeimport/xml:publish', $context)) { 3089 $url = new moodle_url('/grade/import/keymanager.php', array('id'=>$courseid)); 3090 $importplugins['keymanager'] = new grade_plugin_info('keymanager', $url, get_string('keymanager', 'grades')); 3091 } 3092 } 3093 3094 if (count($importplugins) > 0) { 3095 asort($importplugins); 3096 self::$importplugins = $importplugins; 3097 } else { 3098 self::$importplugins = false; 3099 } 3100 return self::$importplugins; 3101 } 3102 /** 3103 * Get information export plugins 3104 * @param int $courseid 3105 * @return array 3106 */ 3107 public static function get_plugins_export($courseid) { 3108 global $CFG; 3109 3110 if (self::$exportplugins !== null) { 3111 return self::$exportplugins; 3112 } 3113 $context = context_course::instance($courseid); 3114 $exportplugins = array(); 3115 $canpublishgrades = 0; 3116 if (has_capability('moodle/grade:export', $context)) { 3117 foreach (core_component::get_plugin_list('gradeexport') as $plugin => $plugindir) { 3118 if (!has_capability('gradeexport/'.$plugin.':view', $context)) { 3119 continue; 3120 } 3121 // All the grade export plugins has grade publishing capabilities. 3122 if (has_capability('gradeexport/'.$plugin.':publish', $context)) { 3123 $canpublishgrades++; 3124 } 3125 3126 $pluginstr = get_string('pluginname', 'gradeexport_'.$plugin); 3127 $url = new moodle_url('/grade/export/'.$plugin.'/index.php', array('id'=>$courseid)); 3128 $exportplugins[$plugin] = new grade_plugin_info($plugin, $url, $pluginstr); 3129 } 3130 3131 // Show key manager if grade publishing is enabled and the user has at least one grade publishing capability. 3132 if ($CFG->gradepublishing && $canpublishgrades != 0) { 3133 $url = new moodle_url('/grade/export/keymanager.php', array('id'=>$courseid)); 3134 $exportplugins['keymanager'] = new grade_plugin_info('keymanager', $url, get_string('keymanager', 'grades')); 3135 } 3136 } 3137 if (count($exportplugins) > 0) { 3138 asort($exportplugins); 3139 self::$exportplugins = $exportplugins; 3140 } else { 3141 self::$exportplugins = false; 3142 } 3143 return self::$exportplugins; 3144 } 3145 3146 /** 3147 * Returns the value of a field from a user record 3148 * 3149 * @param stdClass $user object 3150 * @param stdClass $field object 3151 * @return string value of the field 3152 */ 3153 public static function get_user_field_value($user, $field) { 3154 if (!empty($field->customid)) { 3155 $fieldname = 'customfield_' . $field->customid; 3156 if (!empty($user->{$fieldname}) || is_numeric($user->{$fieldname})) { 3157 $fieldvalue = $user->{$fieldname}; 3158 } else { 3159 $fieldvalue = $field->default; 3160 } 3161 } else { 3162 $fieldvalue = $user->{$field->shortname}; 3163 } 3164 return $fieldvalue; 3165 } 3166 3167 /** 3168 * Returns an array of user profile fields to be included in export 3169 * 3170 * @param int $courseid 3171 * @param bool $includecustomfields 3172 * @return array An array of stdClass instances with customid, shortname, datatype, default and fullname fields 3173 */ 3174 public static function get_user_profile_fields($courseid, $includecustomfields = false) { 3175 global $CFG, $DB; 3176 3177 // Gets the fields that have to be hidden 3178 $hiddenfields = array_map('trim', explode(',', $CFG->hiddenuserfields)); 3179 $context = context_course::instance($courseid); 3180 $canseehiddenfields = has_capability('moodle/course:viewhiddenuserfields', $context); 3181 if ($canseehiddenfields) { 3182 $hiddenfields = array(); 3183 } 3184 3185 $fields = array(); 3186 require_once($CFG->dirroot.'/user/lib.php'); // Loads user_get_default_fields() 3187 require_once($CFG->dirroot.'/user/profile/lib.php'); // Loads constants, such as PROFILE_VISIBLE_ALL 3188 $userdefaultfields = user_get_default_fields(); 3189 3190 // Sets the list of profile fields 3191 $userprofilefields = array_map('trim', explode(',', $CFG->grade_export_userprofilefields)); 3192 if (!empty($userprofilefields)) { 3193 foreach ($userprofilefields as $field) { 3194 $field = trim($field); 3195 if (in_array($field, $hiddenfields) || !in_array($field, $userdefaultfields)) { 3196 continue; 3197 } 3198 $obj = new stdClass(); 3199 $obj->customid = 0; 3200 $obj->shortname = $field; 3201 $obj->fullname = get_string($field); 3202 $fields[] = $obj; 3203 } 3204 } 3205 3206 // Sets the list of custom profile fields 3207 $customprofilefields = array_map('trim', explode(',', $CFG->grade_export_customprofilefields)); 3208 if ($includecustomfields && !empty($customprofilefields)) { 3209 $customfields = profile_get_user_fields_with_data(0); 3210 3211 foreach ($customfields as $fieldobj) { 3212 $field = (object)$fieldobj->get_field_config_for_external(); 3213 // Make sure we can display this custom field 3214 if (!in_array($field->shortname, $customprofilefields)) { 3215 continue; 3216 } else if (in_array($field->shortname, $hiddenfields)) { 3217 continue; 3218 } else if ($field->visible != PROFILE_VISIBLE_ALL && !$canseehiddenfields) { 3219 continue; 3220 } 3221 3222 $obj = new stdClass(); 3223 $obj->customid = $field->id; 3224 $obj->shortname = $field->shortname; 3225 $obj->fullname = format_string($field->name); 3226 $obj->datatype = $field->datatype; 3227 $obj->default = $field->defaultdata; 3228 $fields[] = $obj; 3229 } 3230 } 3231 3232 return $fields; 3233 } 3234 3235 /** 3236 * This helper method gets a snapshot of all the weights for a course. 3237 * It is used as a quick method to see if any wieghts have been automatically adjusted. 3238 * @param int $courseid 3239 * @return array of itemid -> aggregationcoef2 3240 */ 3241 public static function fetch_all_natural_weights_for_course($courseid) { 3242 global $DB; 3243 $result = array(); 3244 3245 $records = $DB->get_records('grade_items', array('courseid'=>$courseid), 'id', 'id, aggregationcoef2'); 3246 foreach ($records as $record) { 3247 $result[$record->id] = $record->aggregationcoef2; 3248 } 3249 return $result; 3250 } 3251 3252 /** 3253 * Resets all static caches. 3254 * 3255 * @return void 3256 */ 3257 public static function reset_caches() { 3258 self::$managesetting = null; 3259 self::$gradereports = null; 3260 self::$gradereportpreferences = null; 3261 self::$scaleinfo = null; 3262 self::$outcomeinfo = null; 3263 self::$letterinfo = null; 3264 self::$importplugins = null; 3265 self::$exportplugins = null; 3266 self::$pluginstrings = null; 3267 self::$aggregationstrings = null; 3268 } 3269 } 3270
title
Description
Body
title
Description
Body
title
Description
Body
title
Body