Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * File containing the grade_report class 19 * 20 * @package core_grades 21 * @copyright 2007 Moodle Pty Ltd (http://moodle.com) 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 use core_user\fields; 26 27 require_once($CFG->libdir.'/gradelib.php'); 28 29 /** 30 * An abstract class containing variables and methods used by all or most reports. 31 * @copyright 2007 Moodle Pty Ltd (http://moodle.com) 32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 */ 34 abstract class grade_report { 35 /** 36 * The courseid. 37 * @var int $courseid 38 */ 39 public $courseid; 40 41 /** 42 * The course. 43 * @var object $course 44 */ 45 public $course; 46 47 /** Grade plugin return tracking object. 48 * @var object $gpr 49 */ 50 public $gpr; 51 52 /** 53 * The context. 54 * 55 * @var context $context 56 */ 57 public $context; 58 59 /** 60 * The grade_tree object. 61 * @var grade_tree $gtree 62 */ 63 public $gtree; 64 65 /** 66 * User preferences related to this report. 67 * @var array $prefs 68 */ 69 public $prefs = array(); 70 71 /** 72 * The roles for this report. 73 * @var string $gradebookroles 74 */ 75 public $gradebookroles; 76 77 /** 78 * base url for sorting by first/last name. 79 * @var string $baseurl 80 */ 81 public $baseurl; 82 83 /** 84 * base url for paging. 85 * @var string $pbarurl 86 */ 87 public $pbarurl; 88 89 /** 90 * Current page (for paging). 91 * @var int $page 92 */ 93 public $page; 94 95 // GROUP VARIABLES (including SQL) 96 97 /** 98 * The current group being displayed. 99 * @var int $currentgroup 100 */ 101 public $currentgroup; 102 103 /** 104 * The current groupname being displayed. 105 * @var string $currentgroupname 106 */ 107 public $currentgroupname; 108 109 /** 110 * Current course group mode 111 * @var int $groupmode 112 */ 113 public $groupmode; 114 115 /** 116 * A HTML select element used to select the current group. 117 * @var string $group_selector 118 */ 119 public $group_selector; 120 121 /** 122 * An SQL fragment used to add linking information to the group tables. 123 * @var string $groupsql 124 */ 125 protected $groupsql; 126 127 /** 128 * An SQL constraint to append to the queries used by this object to build the report. 129 * @var string $groupwheresql 130 */ 131 protected $groupwheresql; 132 133 /** 134 * The ordered params for $groupwheresql 135 * @var array $groupwheresql_params 136 */ 137 protected $groupwheresql_params = array(); 138 139 // USER VARIABLES (including SQL). 140 141 /** 142 * An SQL constraint to append to the queries used by this object to build the report. 143 * @var string $userwheresql 144 */ 145 protected $userwheresql; 146 147 /** 148 * The ordered params for $userwheresql 149 * @var array $userwheresql_params 150 */ 151 protected $userwheresql_params = array(); 152 153 /** 154 * To store user data 155 * @var stdClass $user 156 */ 157 public $user; 158 159 /** 160 * show course/category totals if they contain hidden items 161 * @var array $showtotalsifcontainhidden 162 */ 163 public $showtotalsifcontainhidden = []; 164 165 /** 166 * To store a link to preferences page 167 * @var string $preferences_page 168 */ 169 protected $preferences_page; 170 171 /** 172 * If the user is wanting to search for a particular user within searchable fields their needle will be placed here. 173 * @var string $usersearch 174 */ 175 protected string $usersearch = ''; 176 177 /** 178 * If the user is wanting to show only one particular user their id will be placed here. 179 * @var int $userid 180 */ 181 protected int $userid = -1; 182 183 /** 184 * Constructor. Sets local copies of user preferences and initialises grade_tree. 185 * @param int $courseid 186 * @param object $gpr grade plugin return tracking object 187 * @param string $context 188 * @param int $page The current page being viewed (when report is paged) 189 */ 190 public function __construct($courseid, $gpr, $context, $page=null) { 191 global $CFG, $COURSE, $DB; 192 193 if (empty($CFG->gradebookroles)) { 194 throw new \moodle_exception('norolesdefined', 'grades'); 195 } 196 197 $this->courseid = $courseid; 198 if ($this->courseid == $COURSE->id) { 199 $this->course = $COURSE; 200 } else { 201 $this->course = $DB->get_record('course', array('id' => $this->courseid)); 202 } 203 204 $this->gpr = $gpr; 205 $this->context = $context; 206 $this->page = $page; 207 208 // roles to be displayed in the gradebook 209 $this->gradebookroles = $CFG->gradebookroles; 210 211 // Set up link to preferences page 212 $this->preferences_page = $CFG->wwwroot.'/grade/report/grader/preferences.php?id='.$courseid; 213 214 // init gtree in child class 215 216 // Set any url params. 217 $this->usersearch = optional_param('gpr_search', '', PARAM_NOTAGS); 218 $this->userid = optional_param('gpr_userid', -1, PARAM_INT); 219 } 220 221 /** 222 * Given the name of a user preference (without grade_report_ prefix), locally saves then returns 223 * the value of that preference. If the preference has already been fetched before, 224 * the saved value is returned. If the preference is not set at the User level, the $CFG equivalent 225 * is given (site default). 226 * Can be called statically, but then doesn't benefit from caching 227 * @param string $pref The name of the preference (do not include the grade_report_ prefix) 228 * @param int $objectid An optional itemid or categoryid to check for a more fine-grained preference 229 * @return mixed The value of the preference 230 */ 231 public function get_pref($pref, $objectid=null) { 232 global $CFG; 233 $fullprefname = 'grade_report_' . $pref; 234 $shortprefname = 'grade_' . $pref; 235 236 $retval = null; 237 238 if (!isset($this) OR get_class($this) != 'grade_report') { 239 if (!empty($objectid)) { 240 $retval = get_user_preferences($fullprefname . $objectid, self::get_pref($pref)); 241 } else if (isset($CFG->$fullprefname)) { 242 $retval = get_user_preferences($fullprefname, $CFG->$fullprefname); 243 } else if (isset($CFG->$shortprefname)) { 244 $retval = get_user_preferences($fullprefname, $CFG->$shortprefname); 245 } else { 246 $retval = null; 247 } 248 } else { 249 if (empty($this->prefs[$pref.$objectid])) { 250 251 if (!empty($objectid)) { 252 $retval = get_user_preferences($fullprefname . $objectid); 253 if (empty($retval)) { 254 // No item pref found, we are returning the global preference 255 $retval = $this->get_pref($pref); 256 $objectid = null; 257 } 258 } else { 259 $retval = get_user_preferences($fullprefname, $CFG->$fullprefname); 260 } 261 $this->prefs[$pref.$objectid] = $retval; 262 } else { 263 $retval = $this->prefs[$pref.$objectid]; 264 } 265 } 266 267 return $retval; 268 } 269 270 /** 271 * Uses set_user_preferences() to update the value of a user preference. If 'default' is given as the value, 272 * the preference will be removed in favour of a higher-level preference. 273 * @param string $pref The name of the preference. 274 * @param mixed $pref_value The value of the preference. 275 * @param int $itemid An optional itemid to which the preference will be assigned 276 * @return bool Success or failure. 277 */ 278 public function set_pref($pref, $pref_value='default', $itemid=null) { 279 $fullprefname = 'grade_report_' . $pref; 280 if ($pref_value == 'default') { 281 return unset_user_preference($fullprefname.$itemid); 282 } else { 283 return set_user_preference($fullprefname.$itemid, $pref_value); 284 } 285 } 286 287 /** 288 * Handles form data sent by this report for this report. Abstract method to implement in all children. 289 * @abstract 290 * @param array $data 291 * @return mixed True or array of errors 292 */ 293 abstract public function process_data($data); 294 295 /** 296 * Processes a single action against a category, grade_item or grade. 297 * @param string $target Sortorder 298 * @param string $action Which action to take (edit, delete etc...) 299 * @return 300 */ 301 abstract public function process_action($target, $action); 302 303 /** 304 * Add additional links specific to plugin 305 * @param context_course $context Course context 306 * @param int $courseid Course ID 307 * @param array $element An array representing an element in the grade_tree 308 * @param grade_plugin_return $gpr A grade_plugin_return object 309 * @param string $mode Mode (user or grade item) 310 * @param stdClass $templatecontext Template context 311 * @param bool $otherplugins If we need to insert links to other plugins 312 * @return ?stdClass Updated template context 313 */ 314 public static function get_additional_context(context_course $context, int $courseid, array $element, 315 grade_plugin_return $gpr, string $mode, stdClass $templatecontext, bool $otherplugins = false): ?stdClass { 316 317 if (!$otherplugins) { 318 $component = 'gradereport_' . $gpr->plugin; 319 $params = [$context, $courseid, $element, $gpr, $mode, $templatecontext]; 320 return component_callback($component, 'get_report_link', $params); 321 } else { 322 // Loop through all installed grade reports. 323 foreach (core_component::get_plugin_list('gradereport') as $plugin => $plugindir) { 324 $params = [$context, $courseid, $element, $gpr, $mode, $templatecontext]; 325 $component = 'gradereport_' . $plugin; 326 $templatecontextupdated = component_callback($component, 'get_report_link', $params); 327 if ($templatecontextupdated) { 328 $templatecontext = $templatecontextupdated; 329 } 330 } 331 return $templatecontext; 332 } 333 } 334 335 /** 336 * First checks the cached language strings, then returns match if found, or uses get_string() 337 * to get it from the DB, caches it then returns it. 338 * 339 * @deprecated since 4.2 340 * @todo MDL-77307 This will be deleted in Moodle 4.6. 341 * @param string $strcode 342 * @param string $section Optional language section 343 * @return string 344 */ 345 public function get_lang_string($strcode, $section=null) { 346 debugging('grade_report::get_lang_string() is deprecated, please use' . 347 ' get_string() instead.', DEBUG_DEVELOPER); 348 349 if (empty($this->lang_strings[$strcode])) { 350 $this->lang_strings[$strcode] = get_string($strcode, $section); 351 } 352 return $this->lang_strings[$strcode]; 353 } 354 355 /** 356 * Fetches and returns a count of all the users that will be shown on this page. 357 * @param boolean $groups include groups limit 358 * @param boolean $users include users limit - default false, used for searching purposes 359 * @return int Count of users 360 */ 361 public function get_numusers($groups = true, $users = false) { 362 global $CFG, $DB; 363 $userwheresql = ""; 364 $groupsql = ""; 365 $groupwheresql = ""; 366 367 // Limit to users with a gradeable role. 368 list($gradebookrolessql, $gradebookrolesparams) = $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0'); 369 370 // Limit to users with an active enrollment. 371 list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context); 372 373 // We want to query both the current context and parent contexts. 374 list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx'); 375 376 $params = array_merge($gradebookrolesparams, $enrolledparams, $relatedctxparams); 377 378 if ($users) { 379 $userwheresql = $this->userwheresql; 380 $params = array_merge($params, $this->userwheresql_params); 381 } 382 383 if ($groups) { 384 $groupsql = $this->groupsql; 385 $groupwheresql = $this->groupwheresql; 386 $params = array_merge($params, $this->groupwheresql_params); 387 } 388 389 $sql = "SELECT DISTINCT u.id 390 FROM {user} u 391 JOIN ($enrolledsql) je 392 ON je.id = u.id 393 JOIN {role_assignments} ra 394 ON u.id = ra.userid 395 $groupsql 396 WHERE ra.roleid $gradebookrolessql 397 AND u.deleted = 0 398 $userwheresql 399 $groupwheresql 400 AND ra.contextid $relatedctxsql"; 401 $selectedusers = $DB->get_records_sql($sql, $params); 402 403 $count = 0; 404 // Check if user's enrolment is active and should be displayed. 405 if (!empty($selectedusers)) { 406 $coursecontext = $this->context->get_course_context(true); 407 408 $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol); 409 $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol); 410 $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $coursecontext); 411 412 if ($showonlyactiveenrol) { 413 $useractiveenrolments = get_enrolled_users($coursecontext, '', 0, 'u.id', null, 0, 0, true); 414 } 415 416 foreach ($selectedusers as $id => $value) { 417 if (!$showonlyactiveenrol || ($showonlyactiveenrol && array_key_exists($id, $useractiveenrolments))) { 418 $count++; 419 } 420 } 421 } 422 return $count; 423 } 424 425 /** 426 * Shows support for being used as a 'Grades' report. 427 */ 428 public static function supports_mygrades() { 429 return false; 430 } 431 432 /** 433 * Sets up this object's group variables, mainly to restrict the selection of users to display. 434 */ 435 protected function setup_groups() { 436 // find out current groups mode 437 if ($this->groupmode = groups_get_course_groupmode($this->course)) { 438 if (empty($this->gpr->groupid)) { 439 $this->currentgroup = groups_get_course_group($this->course, true); 440 } else { 441 $this->currentgroup = $this->gpr->groupid; 442 } 443 $this->group_selector = groups_print_course_menu($this->course, $this->pbarurl, true); 444 445 if ($this->groupmode == SEPARATEGROUPS and !$this->currentgroup and !has_capability('moodle/site:accessallgroups', $this->context)) { 446 $this->currentgroup = -2; // means can not access any groups at all 447 } 448 if ($this->currentgroup) { 449 if ($group = groups_get_group($this->currentgroup)) { 450 $this->currentgroupname = $group->name; 451 } 452 $this->groupsql = " JOIN {groups_members} gm ON gm.userid = u.id "; 453 $this->groupwheresql = " AND gm.groupid = :gr_grpid "; 454 $this->groupwheresql_params = array('gr_grpid'=>$this->currentgroup); 455 } 456 } 457 } 458 459 /** 460 * Sets up this report's user criteria to restrict the selection of users to display. 461 */ 462 public function setup_users() { 463 global $SESSION, $DB; 464 465 $filterfirstnamekey = "filterfirstname-{$this->context->id}"; 466 $filtersurnamekey = "filtersurname-{$this->context->id}"; 467 468 $this->userwheresql = ""; 469 $this->userwheresql_params = array(); 470 if (!empty($SESSION->gradereport[$filterfirstnamekey])) { 471 $this->userwheresql .= ' AND '.$DB->sql_like('u.firstname', ':firstname', false, false); 472 $this->userwheresql_params['firstname'] = $SESSION->gradereport[$filterfirstnamekey] . '%'; 473 } 474 if (!empty($SESSION->gradereport[$filtersurnamekey])) { 475 $this->userwheresql .= ' AND '.$DB->sql_like('u.lastname', ':lastname', false, false); 476 $this->userwheresql_params['lastname'] = $SESSION->gradereport[$filtersurnamekey] . '%'; 477 } 478 479 // When a user wants to view a particular user rather than a set of users. 480 // By omission when selecting one user, also allow passing the search value around. 481 if ($this->userid !== -1) { 482 $this->userwheresql .= " AND u.id = :uid"; 483 $this->userwheresql_params['uid'] = $this->userid; 484 } 485 486 // A user wants to return a subset of learners that match their search criteria. 487 if ($this->usersearch !== '' && $this->userid === -1) { 488 // Get the fields for all contexts because there is a special case later where it allows 489 // matches of fields you can't access if they are on your own account. 490 $userfields = fields::for_identity(null, false)->with_userpic(); 491 ['mappings' => $mappings] = (array)$userfields->get_sql('u', true); 492 [ 493 'where' => $keywordswhere, 494 'params' => $keywordsparams, 495 ] = $this->get_users_search_sql($mappings, (array)$userfields); 496 $this->userwheresql .= " AND $keywordswhere"; 497 $this->userwheresql_params = array_merge($this->userwheresql_params, $keywordsparams); 498 } 499 } 500 501 /** 502 * Prepare SQL where clause and associated parameters for any user searching being performed. 503 * This mostly came from core_user\table\participants_search with some slight modifications four our use case. 504 * 505 * @param array $mappings Array of field mappings (fieldname => SQL code for the value) 506 * @param array $userfields An array that we cast from user profile fields to search within. 507 * @return array SQL query data in the format ['where' => '', 'params' => []]. 508 */ 509 protected function get_users_search_sql(array $mappings, array $userfields): array { 510 global $DB, $USER; 511 512 $canviewfullnames = has_capability('moodle/site:viewfullnames', $this->context); 513 514 $params = []; 515 $searchkey1 = 'search01'; 516 $searchkey2 = 'search02'; 517 $searchkey3 = 'search03'; 518 519 $conditions = []; 520 521 // Search by fullname. 522 [$fullname, $fullnameparams] = fields::get_sql_fullname('u', $canviewfullnames); 523 $conditions[] = $DB->sql_like($fullname, ':' . $searchkey1, false, false); 524 $params = array_merge($params, $fullnameparams); 525 526 // Search by email. 527 $email = $DB->sql_like('email', ':' . $searchkey2, false, false); 528 529 if (!in_array('email', $userfields)) { 530 $maildisplay = 'maildisplay0'; 531 $userid1 = 'userid01'; 532 // Prevent users who hide their email address from being found by others 533 // who aren't allowed to see hidden email addresses. 534 $email = "(". $email ." AND (" . 535 "u.maildisplay <> :$maildisplay " . 536 "OR u.id = :$userid1". // Users can always find themselves. 537 "))"; 538 $params[$maildisplay] = core_user::MAILDISPLAY_HIDE; 539 $params[$userid1] = $USER->id; 540 } 541 542 $conditions[] = $email; 543 544 // Search by idnumber. 545 $idnumber = $DB->sql_like('idnumber', ':' . $searchkey3, false, false); 546 547 if (!in_array('idnumber', $userfields)) { 548 $userid2 = 'userid02'; 549 // Users who aren't allowed to see idnumbers should at most find themselves 550 // when searching for an idnumber. 551 $idnumber = "(". $idnumber . " AND u.id = :$userid2)"; 552 $params[$userid2] = $USER->id; 553 } 554 555 $conditions[] = $idnumber; 556 557 // Search all user identify fields. 558 $extrasearchfields = fields::get_identity_fields(null, false); 559 foreach ($extrasearchfields as $fieldindex => $extrasearchfield) { 560 if (in_array($extrasearchfield, ['email', 'idnumber', 'country'])) { 561 // Already covered above. 562 continue; 563 } 564 // The param must be short (max 32 characters) so don't include field name. 565 $param = $searchkey3 . '_ident' . $fieldindex; 566 $fieldsql = $mappings[$extrasearchfield]; 567 $condition = $DB->sql_like($fieldsql, ':' . $param, false, false); 568 $params[$param] = "%$this->usersearch%"; 569 570 if (!in_array($extrasearchfield, $userfields)) { 571 // User cannot see this field, but allow match if their own account. 572 $userid3 = 'userid03_ident' . $fieldindex; 573 $condition = "(". $condition . " AND u.id = :$userid3)"; 574 $params[$userid3] = $USER->id; 575 } 576 $conditions[] = $condition; 577 } 578 579 $where = "(". implode(" OR ", $conditions) .") "; 580 $params[$searchkey1] = "%$this->usersearch%"; 581 $params[$searchkey2] = "%$this->usersearch%"; 582 $params[$searchkey3] = "%$this->usersearch%"; 583 584 return [ 585 'where' => $where, 586 'params' => $params, 587 ]; 588 } 589 590 /** 591 * Returns an arrow icon inside an <a> tag, for the purpose of sorting a column. 592 * @param string $direction 593 * @param moodle_url|null $sortlink 594 */ 595 protected function get_sort_arrow(string $direction = 'down', ?moodle_url $sortlink = null) { 596 global $OUTPUT; 597 $pix = ['up' => 't/sort_desc', 'down' => 't/sort_asc']; 598 $matrix = ['up' => 'desc', 'down' => 'asc']; 599 $strsort = get_string($matrix[$direction], 'moodle'); 600 $arrow = $OUTPUT->pix_icon($pix[$direction], '', '', ['class' => 'sorticon']); 601 602 if (!empty($sortlink)) { 603 $sortlink->param('sort', ($direction == 'up' ? 'asc' : 'desc')); 604 } 605 606 return html_writer::link($sortlink, $arrow, ['title' => $strsort, 'aria-label' => $strsort, 'data-collapse' => 'sort', 607 'class' => 'arrow_link py-1']); 608 } 609 610 /** 611 * Optionally blank out course/category totals if they contain any hidden items 612 * @param string $courseid the course id 613 * @param string $course_item an instance of grade_item 614 * @param string $finalgrade the grade for the course_item 615 * @return array[] containing values for 'grade', 'grademax', 'grademin', 'aggregationstatus' and 'aggregationweight' 616 */ 617 protected function blank_hidden_total_and_adjust_bounds($courseid, $course_item, $finalgrade) { 618 global $CFG, $DB; 619 static $hiding_affected = null;//array of items in this course affected by hiding 620 621 // If we're dealing with multiple users we need to know when we've moved on to a new user. 622 static $previous_userid = null; 623 624 // If we're dealing with multiple courses we need to know when we've moved on to a new course. 625 static $previous_courseid = null; 626 627 $coursegradegrade = grade_grade::fetch(array('userid'=>$this->user->id, 'itemid'=>$course_item->id)); 628 $grademin = $course_item->grademin; 629 $grademax = $course_item->grademax; 630 if ($coursegradegrade) { 631 $grademin = $coursegradegrade->get_grade_min(); 632 $grademax = $coursegradegrade->get_grade_max(); 633 } else { 634 $coursegradegrade = new grade_grade(array('userid'=>$this->user->id, 'itemid'=>$course_item->id), false); 635 } 636 $hint = $coursegradegrade->get_aggregation_hint(); 637 $aggregationstatus = $hint['status']; 638 $aggregationweight = $hint['weight']; 639 640 if (!is_array($this->showtotalsifcontainhidden)) { 641 debugging('showtotalsifcontainhidden should be an array', DEBUG_DEVELOPER); 642 $this->showtotalsifcontainhidden = array($courseid => $this->showtotalsifcontainhidden); 643 } 644 645 if ($this->showtotalsifcontainhidden[$courseid] == GRADE_REPORT_SHOW_REAL_TOTAL_IF_CONTAINS_HIDDEN) { 646 return array('grade' => $finalgrade, 647 'grademin' => $grademin, 648 'grademax' => $grademax, 649 'aggregationstatus' => $aggregationstatus, 650 'aggregationweight' => $aggregationweight); 651 } 652 653 // If we've moved on to another course or user, reload the grades. 654 if ($previous_userid != $this->user->id || $previous_courseid != $courseid) { 655 $hiding_affected = null; 656 $previous_userid = $this->user->id; 657 $previous_courseid = $courseid; 658 } 659 660 if (!$hiding_affected) { 661 $items = grade_item::fetch_all(array('courseid'=>$courseid)); 662 $grades = array(); 663 $sql = "SELECT g.* 664 FROM {grade_grades} g 665 JOIN {grade_items} gi ON gi.id = g.itemid 666 WHERE g.userid = {$this->user->id} AND gi.courseid = {$courseid}"; 667 if ($gradesrecords = $DB->get_records_sql($sql)) { 668 foreach ($gradesrecords as $grade) { 669 $grades[$grade->itemid] = new grade_grade($grade, false); 670 } 671 unset($gradesrecords); 672 } 673 foreach ($items as $itemid => $unused) { 674 if (!isset($grades[$itemid])) { 675 $grade_grade = new grade_grade(); 676 $grade_grade->userid = $this->user->id; 677 $grade_grade->itemid = $items[$itemid]->id; 678 $grades[$itemid] = $grade_grade; 679 } 680 $grades[$itemid]->grade_item =& $items[$itemid]; 681 } 682 $hiding_affected = grade_grade::get_hiding_affected($grades, $items); 683 } 684 685 //if the item definitely depends on a hidden item 686 if (array_key_exists($course_item->id, $hiding_affected['altered']) || 687 array_key_exists($course_item->id, $hiding_affected['alteredgrademin']) || 688 array_key_exists($course_item->id, $hiding_affected['alteredgrademax']) || 689 array_key_exists($course_item->id, $hiding_affected['alteredaggregationstatus']) || 690 array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) { 691 if (!$this->showtotalsifcontainhidden[$courseid] && array_key_exists($course_item->id, $hiding_affected['altered'])) { 692 // Hide the grade, but only when it has changed. 693 $finalgrade = null; 694 } else { 695 //use reprocessed marks that exclude hidden items 696 if (array_key_exists($course_item->id, $hiding_affected['altered'])) { 697 $finalgrade = $hiding_affected['altered'][$course_item->id]; 698 } 699 if (array_key_exists($course_item->id, $hiding_affected['alteredgrademin'])) { 700 $grademin = $hiding_affected['alteredgrademin'][$course_item->id]; 701 } 702 if (array_key_exists($course_item->id, $hiding_affected['alteredgrademax'])) { 703 $grademax = $hiding_affected['alteredgrademax'][$course_item->id]; 704 } 705 if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationstatus'])) { 706 $aggregationstatus = $hiding_affected['alteredaggregationstatus'][$course_item->id]; 707 } 708 if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) { 709 $aggregationweight = $hiding_affected['alteredaggregationweight'][$course_item->id]; 710 } 711 712 if (!$this->showtotalsifcontainhidden[$courseid]) { 713 // If the course total is hidden we must hide the weight otherwise 714 // it can be used to compute the course total. 715 $aggregationstatus = 'unknown'; 716 $aggregationweight = null; 717 } 718 } 719 } else if (array_key_exists($course_item->id, $hiding_affected['unknowngrades'])) { 720 //not sure whether or not this item depends on a hidden item 721 if (!$this->showtotalsifcontainhidden[$courseid]) { 722 //hide the grade 723 $finalgrade = null; 724 } else { 725 //use reprocessed marks that exclude hidden items 726 $finalgrade = $hiding_affected['unknowngrades'][$course_item->id]; 727 728 if (array_key_exists($course_item->id, $hiding_affected['alteredgrademin'])) { 729 $grademin = $hiding_affected['alteredgrademin'][$course_item->id]; 730 } 731 if (array_key_exists($course_item->id, $hiding_affected['alteredgrademax'])) { 732 $grademax = $hiding_affected['alteredgrademax'][$course_item->id]; 733 } 734 if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationstatus'])) { 735 $aggregationstatus = $hiding_affected['alteredaggregationstatus'][$course_item->id]; 736 } 737 if (array_key_exists($course_item->id, $hiding_affected['alteredaggregationweight'])) { 738 $aggregationweight = $hiding_affected['alteredaggregationweight'][$course_item->id]; 739 } 740 } 741 } 742 743 return array('grade' => $finalgrade, 'grademin' => $grademin, 'grademax' => $grademax, 'aggregationstatus'=>$aggregationstatus, 'aggregationweight'=>$aggregationweight); 744 } 745 746 /** 747 * Optionally blank out course/category totals if they contain any hidden items 748 * @deprecated since Moodle 2.8 - Call blank_hidden_total_and_adjust_bounds instead. 749 * @param string $courseid the course id 750 * @param string $course_item an instance of grade_item 751 * @param string $finalgrade the grade for the course_item 752 * @return string The new final grade 753 */ 754 protected function blank_hidden_total($courseid, $course_item, $finalgrade) { 755 // Note it is flawed to call this function directly because 756 // the aggregated grade does not make sense without the updated min and max information. 757 758 debugging('grade_report::blank_hidden_total() is deprecated. 759 Call grade_report::blank_hidden_total_and_adjust_bounds instead.', DEBUG_DEVELOPER); 760 $result = $this->blank_hidden_total_and_adjust_bounds($courseid, $course_item, $finalgrade); 761 return $result['grade']; 762 } 763 764 /** 765 * Calculate average grade for a given grade item. 766 * Based on calculate_averages function from grade/report/user/lib.php 767 * 768 * @param grade_item $gradeitem Grade item 769 * @param array $info Ungraded grade items counts and report preferences. 770 * @return array Average grade and meancount. 771 */ 772 public static function calculate_average(grade_item $gradeitem, array $info): array { 773 774 $meanselection = $info['report']['meanselection']; 775 $totalcount = $info['report']['totalcount']; 776 $ungradedcounts = $info['ungradedcounts']; 777 $sumarray = $info['sumarray']; 778 779 if (empty($sumarray[$gradeitem->id])) { 780 $sumarray[$gradeitem->id] = 0; 781 } 782 783 if (empty($ungradedcounts[$gradeitem->id])) { 784 $ungradedcounts = 0; 785 } else { 786 $ungradedcounts = $ungradedcounts[$gradeitem->id]->count; 787 } 788 789 // If they want the averages to include all grade items. 790 if ($meanselection == GRADE_REPORT_MEAN_GRADED) { 791 $meancount = $totalcount - $ungradedcounts; 792 } else { 793 // Bump up the sum by the number of ungraded items * grademin. 794 $sumarray[$gradeitem->id] += ($ungradedcounts * $gradeitem->grademin); 795 $meancount = $totalcount; 796 } 797 798 $aggr['meancount'] = $meancount; 799 800 if (empty($sumarray[$gradeitem->id]) || $meancount == 0) { 801 $aggr['average'] = null; 802 } else { 803 $sum = $sumarray[$gradeitem->id]; 804 $aggr['average'] = $sum / $meancount; 805 } 806 return $aggr; 807 } 808 809 /** 810 * Get ungraded grade items info and sum of all grade items in a course. 811 * Based on calculate_averages function from grade/report/user/lib.php 812 * 813 * @return array Ungraded grade items counts with report preferences. 814 */ 815 public function ungraded_counts(): array { 816 global $DB; 817 818 $groupid = null; 819 if (isset($this->gpr->groupid)) { 820 $groupid = $this->gpr->groupid; 821 } 822 823 $info = []; 824 $info['report'] = [ 825 'averagesdisplaytype' => $this->get_pref('averagesdisplaytype'), 826 'averagesdecimalpoints' => $this->get_pref('averagesdecimalpoints'), 827 'meanselection' => $this->get_pref('meanselection'), 828 'shownumberofgrades' => $this->get_pref('shownumberofgrades'), 829 'totalcount' => $this->get_numusers(!is_null($groupid)), 830 ]; 831 832 // We want to query both the current context and parent contexts. 833 list($relatedctxsql, $relatedctxparams) = 834 $DB->get_in_or_equal($this->context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx'); 835 836 // Limit to users with a gradeable role ie students. 837 list($gradebookrolessql, $gradebookrolesparams) = 838 $DB->get_in_or_equal(explode(',', $this->gradebookroles), SQL_PARAMS_NAMED, 'grbr0'); 839 840 // Limit to users with an active enrolment. 841 $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol); 842 $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol); 843 $showonlyactiveenrol = $showonlyactiveenrol || 844 !has_capability('moodle/course:viewsuspendedusers', $this->context); 845 list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context, '', 0, $showonlyactiveenrol); 846 847 $params = array_merge($this->groupwheresql_params, $gradebookrolesparams, $enrolledparams, $relatedctxparams); 848 $params['courseid'] = $this->courseid; 849 850 // Aggregate on whole course only. 851 if (empty($groupid)) { 852 $this->groupsql = null; 853 $this->groupwheresql = null; 854 } 855 856 // Empty grades must be evaluated as grademin, NOT always 0. 857 // This query returns a count of ungraded grades (NULL finalgrade OR no matching record in grade_grades table). 858 // No join condition when joining grade_items and user to get a grade item row for every user. 859 // Then left join with grade_grades and look for rows with null final grade 860 // (which includes grade items with no grade_grade). 861 $sql = "SELECT gi.id, COUNT(u.id) AS count 862 FROM {grade_items} gi 863 JOIN {user} u ON u.deleted = 0 864 JOIN ($enrolledsql) je ON je.id = u.id 865 JOIN ( 866 SELECT DISTINCT ra.userid 867 FROM {role_assignments} ra 868 WHERE ra.roleid $gradebookrolessql 869 AND ra.contextid $relatedctxsql 870 ) rainner ON rainner.userid = u.id 871 LEFT JOIN {grade_grades} gg 872 ON (gg.itemid = gi.id AND gg.userid = u.id AND gg.finalgrade IS NOT NULL AND gg.hidden = 0) 873 $this->groupsql 874 WHERE gi.courseid = :courseid 875 AND gg.finalgrade IS NULL 876 $this->groupwheresql 877 GROUP BY gi.id"; 878 $info['ungradedcounts'] = $DB->get_records_sql($sql, $params); 879 880 // Find sums of all grade items in course. 881 $sql = "SELECT gg.itemid, SUM(gg.finalgrade) AS sum 882 FROM {grade_items} gi 883 JOIN {grade_grades} gg ON gg.itemid = gi.id 884 JOIN {user} u ON u.id = gg.userid 885 JOIN ($enrolledsql) je ON je.id = gg.userid 886 JOIN ( 887 SELECT DISTINCT ra.userid 888 FROM {role_assignments} ra 889 WHERE ra.roleid $gradebookrolessql 890 AND ra.contextid $relatedctxsql 891 ) rainner ON rainner.userid = u.id 892 $this->groupsql 893 WHERE gi.courseid = :courseid 894 AND u.deleted = 0 895 AND gg.finalgrade IS NOT NULL 896 AND gg.hidden = 0 897 $this->groupwheresql 898 GROUP BY gg.itemid"; 899 900 $sumarray = []; 901 $sums = $DB->get_recordset_sql($sql, $params); 902 foreach ($sums as $itemid => $csum) { 903 $sumarray[$itemid] = grade_floatval($csum->sum); 904 } 905 $sums->close(); 906 $info['sumarray'] = $sumarray; 907 908 return $info; 909 } 910 911 /** 912 * Get grade item type names in a course to use in filter dropdown. 913 * 914 * @return array Item types. 915 */ 916 public function item_types(): array { 917 global $DB, $CFG; 918 919 $modnames = []; 920 $sql = "(SELECT gi.itemmodule 921 FROM {grade_items} gi 922 WHERE gi.courseid = :courseid1 923 AND gi.itemmodule IS NOT NULL) 924 UNION 925 (SELECT gi1.itemtype 926 FROM {grade_items} gi1 927 WHERE gi1.courseid = :courseid2 928 AND gi1.itemtype = 'manual')"; 929 930 $itemtypes = $DB->get_records_sql($sql, ['courseid1' => $this->courseid, 'courseid2' => $this->courseid]); 931 foreach ($itemtypes as $itemtype => $value) { 932 if (file_exists("$CFG->dirroot/mod/$itemtype/lib.php")) { 933 $modnames[$itemtype] = get_string("modulename", $itemtype, null, true); 934 } else if ($itemtype == 'manual') { 935 $modnames[$itemtype] = get_string('manualitem', 'grades', null, true); 936 } 937 } 938 939 return $modnames; 940 } 941 942 /** 943 * Load a valid list of gradable users in a course. 944 * 945 * @param int $courseid The course ID. 946 * @param int|null $groupid The group ID (optional). 947 * @return array A list of enrolled gradable users. 948 */ 949 public static function get_gradable_users(int $courseid, ?int $groupid = null): array { 950 global $CFG; 951 require_once($CFG->dirroot . '/grade/lib.php'); 952 953 $context = context_course::instance($courseid); 954 $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol); 955 $onlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol) || 956 !has_capability('moodle/course:viewsuspendedusers', $context); 957 958 return get_gradable_users($courseid, $groupid, $onlyactiveenrol); 959 } 960 961 } 962
title
Description
Body
title
Description
Body
title
Description
Body
title
Body