See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 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 * Renderable class for gradehistory report. 19 * 20 * @package gradereport_history 21 * @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace gradereport_history\output; 26 27 defined('MOODLE_INTERNAL') || die; 28 29 require_once($CFG->libdir . '/tablelib.php'); 30 require_once($CFG->dirroot . '/user/lib.php'); 31 32 /** 33 * Renderable class for gradehistory report. 34 * 35 * @since Moodle 2.8 36 * @package gradereport_history 37 * @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com> 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40 class tablelog extends \table_sql implements \renderable { 41 42 /** 43 * @var int course id. 44 */ 45 protected $courseid; 46 47 /** 48 * @var \context context of the page to be rendered. 49 */ 50 protected $context; 51 52 /** 53 * @var \stdClass A list of filters to be applied to the sql query. 54 */ 55 protected $filters; 56 57 /** 58 * @var \stdClass[] List of users included in the report (if userids are specified as filters) 59 */ 60 protected $users = []; 61 62 /** 63 * @var array A list of grade items present in the course. 64 */ 65 protected $gradeitems = array(); 66 67 /** 68 * @var \course_modinfo|null A list of cm instances in course. 69 */ 70 protected $cms; 71 72 /** 73 * @var int The default number of decimal points to use in this course 74 * when a grade item does not itself define the number of decimal points. 75 */ 76 protected $defaultdecimalpoints; 77 78 /** 79 * Sets up the table_log parameters. 80 * 81 * @param string $uniqueid unique id of table. 82 * @param \context_course $context Context of the report. 83 * @param \moodle_url $url url of the page where this table would be displayed. 84 * @param array $filters options are: 85 * userids : limit to specific users (default: none) 86 * itemid : limit to specific grade item (default: all) 87 * grader : limit to specific graders (default: all) 88 * datefrom : start of date range 89 * datetill : end of date range 90 * revisedonly : only show revised grades (default: false) 91 * format : page | csv | excel (default: page) 92 * @param string $download Represents download format, pass '' no download at this time. 93 * @param int $page The current page being displayed. 94 * @param int $perpage Number of rules to display per page. 95 */ 96 public function __construct($uniqueid, \context_course $context, $url, $filters = array(), $download = '', $page = 0, 97 $perpage = 100) { 98 global $CFG; 99 parent::__construct($uniqueid); 100 101 $this->set_attribute('class', 'gradereport_history generaltable generalbox'); 102 103 // Set protected properties. 104 $this->context = $context; 105 $this->courseid = $this->context->instanceid; 106 $this->pagesize = $perpage; 107 $this->page = $page; 108 $this->gradeitems = \grade_item::fetch_all(array('courseid' => $this->courseid)); 109 $this->cms = get_fast_modinfo($this->courseid); 110 $this->useridfield = 'userid'; 111 $this->defaultdecimalpoints = grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints); 112 113 // Define columns in the table. 114 $this->define_table_columns(); 115 116 // Define filters. 117 $this->define_table_filters((object) $filters); 118 119 // Define configs. 120 $this->define_table_configs($url); 121 122 // Set download status. 123 $this->is_downloading($download, get_string('exportfilename', 'gradereport_history')); 124 } 125 126 /** 127 * Define table configs. 128 * 129 * @param \moodle_url $url url of the page where this table would be displayed. 130 */ 131 protected function define_table_configs(\moodle_url $url) { 132 133 // Set table url. 134 $urlparams = (array)$this->filters; 135 unset($urlparams['submitbutton']); 136 unset($urlparams['userfullnames']); 137 $url->params($urlparams); 138 $this->define_baseurl($url); 139 140 // Set table configs. 141 $this->collapsible(true); 142 $this->sortable(true, 'timemodified', SORT_DESC); 143 $this->pageable(true); 144 $this->no_sorting('grader'); 145 } 146 147 /** 148 * Define table filters 149 * 150 * @param \stdClass $filters 151 */ 152 protected function define_table_filters(\stdClass $filters): void { 153 global $DB; 154 155 $this->filters = $filters; 156 157 if (!empty($this->filters->userids)) { 158 159 $course = get_course($this->courseid); 160 161 // Retrieve userids that are part of the filters object, and ensure user can access each of them. 162 [$userselect, $userparams] = $DB->get_in_or_equal(explode(',', $this->filters->userids), SQL_PARAMS_NAMED); 163 [$usersort] = users_order_by_sql(); 164 165 $this->users = array_filter( 166 $DB->get_records_select('user', "id {$userselect}", $userparams, $usersort), 167 static function(\stdClass $user) use ($course): bool { 168 return user_can_view_profile($user, $course); 169 } 170 ); 171 172 // Reset userids to the filtered array of users. 173 $this->filters->userids = implode(',', array_keys($this->users)); 174 } 175 } 176 177 /** 178 * Setup the headers for the html table. 179 */ 180 protected function define_table_columns() { 181 $extrafields = \core_user\fields::get_identity_fields($this->context); 182 183 // Define headers and columns. 184 $cols = array( 185 'timemodified' => get_string('datetime', 'gradereport_history'), 186 'fullname' => get_string('name') 187 ); 188 189 // Add headers for extra user fields. 190 foreach ($extrafields as $field) { 191 if (get_string_manager()->string_exists($field, 'moodle')) { 192 $cols[$field] = get_string($field); 193 } else { 194 $cols[$field] = \core_user\fields::get_display_name($field); 195 } 196 } 197 198 // Add remaining headers. 199 $cols = array_merge($cols, array( 200 'itemname' => get_string('gradeitem', 'grades'), 201 'prevgrade' => get_string('gradeold', 'gradereport_history'), 202 'finalgrade' => get_string('gradenew', 'gradereport_history'), 203 'grader' => get_string('grader', 'gradereport_history'), 204 'source' => get_string('source', 'gradereport_history'), 205 'overridden' => get_string('overridden', 'grades'), 206 'locked' => get_string('locked', 'grades'), 207 'excluded' => get_string('excluded', 'gradereport_history'), 208 'feedback' => get_string('feedbacktext', 'gradereport_history') 209 ) 210 ); 211 212 $this->define_columns(array_keys($cols)); 213 $this->define_headers(array_values($cols)); 214 } 215 216 /** 217 * Method to display the final grade. 218 * 219 * @param \stdClass $history an entry of history record. 220 * 221 * @return string HTML to display 222 */ 223 public function col_finalgrade(\stdClass $history) { 224 if (!empty($this->gradeitems[$history->itemid])) { 225 $decimalpoints = $this->gradeitems[$history->itemid]->get_decimals(); 226 } else { 227 $decimalpoints = $this->defaultdecimalpoints; 228 } 229 230 return format_float($history->finalgrade, $decimalpoints); 231 } 232 233 /** 234 * Method to display the previous grade. 235 * 236 * @param \stdClass $history an entry of history record. 237 * 238 * @return string HTML to display 239 */ 240 public function col_prevgrade(\stdClass $history) { 241 if (!empty($this->gradeitems[$history->itemid])) { 242 $decimalpoints = $this->gradeitems[$history->itemid]->get_decimals(); 243 } else { 244 $decimalpoints = $this->defaultdecimalpoints; 245 } 246 247 return format_float($history->prevgrade, $decimalpoints); 248 } 249 250 /** 251 * Method to display column timemodifed. 252 * 253 * @param \stdClass $history an entry of history record. 254 * 255 * @return string HTML to display 256 */ 257 public function col_timemodified(\stdClass $history) { 258 return userdate($history->timemodified); 259 } 260 261 /** 262 * Method to display column itemname. 263 * 264 * @param \stdClass $history an entry of history record. 265 * 266 * @return string HTML to display 267 */ 268 public function col_itemname(\stdClass $history) { 269 // Make sure grade item is still present and link it to the module if possible. 270 $itemid = $history->itemid; 271 if (!empty($this->gradeitems[$itemid])) { 272 if ($history->itemtype === 'mod' && !$this->is_downloading()) { 273 if (!empty($this->cms->instances[$history->itemmodule][$history->iteminstance])) { 274 $cm = $this->cms->instances[$history->itemmodule][$history->iteminstance]; 275 $url = new \moodle_url('/mod/' . $history->itemmodule . '/view.php', array('id' => $cm->id)); 276 return \html_writer::link($url, $this->gradeitems[$itemid]->get_name()); 277 } 278 } 279 return $this->gradeitems[$itemid]->get_name(); 280 } 281 return get_string('deleteditemid', 'gradereport_history', $history->itemid); 282 } 283 284 /** 285 * Method to display column grader. 286 * 287 * @param \stdClass $history an entry of history record. 288 * 289 * @return string HTML to display 290 */ 291 public function col_grader(\stdClass $history) { 292 if (empty($history->usermodified)) { 293 // Not every row has a valid usermodified. 294 return ''; 295 } 296 297 $grader = new \stdClass(); 298 $grader = username_load_fields_from_object($grader, $history, 'grader'); 299 $name = fullname($grader); 300 301 if ($this->download) { 302 return $name; 303 } 304 305 $userid = $history->usermodified; 306 $profileurl = new \moodle_url('/user/view.php', array('id' => $userid, 'course' => $this->courseid)); 307 308 return \html_writer::link($profileurl, $name); 309 } 310 311 /** 312 * Method to display column overridden. 313 * 314 * @param \stdClass $history an entry of history record. 315 * 316 * @return string HTML to display 317 */ 318 public function col_overridden(\stdClass $history) { 319 return $history->overridden ? get_string('yes') : get_string('no'); 320 } 321 322 /** 323 * Method to display column locked. 324 * 325 * @param \stdClass $history an entry of history record. 326 * 327 * @return string HTML to display 328 */ 329 public function col_locked(\stdClass $history) { 330 return $history->locked ? get_string('yes') : get_string('no'); 331 } 332 333 /** 334 * Method to display column excluded. 335 * 336 * @param \stdClass $history an entry of history record. 337 * 338 * @return string HTML to display 339 */ 340 public function col_excluded(\stdClass $history) { 341 return $history->excluded ? get_string('yes') : get_string('no'); 342 } 343 344 /** 345 * Method to display column feedback. 346 * 347 * @param \stdClass $history an entry of history record. 348 * 349 * @return string HTML to display 350 */ 351 public function col_feedback(\stdClass $history) { 352 if ($this->is_downloading()) { 353 return $history->feedback; 354 } else { 355 // We need the activity context, not the course context. 356 $gradeitem = $this->gradeitems[$history->itemid]; 357 $context = $gradeitem->get_context(); 358 359 $feedback = file_rewrite_pluginfile_urls( 360 $history->feedback, 361 'pluginfile.php', 362 $context->id, 363 GRADE_FILE_COMPONENT, 364 GRADE_HISTORY_FEEDBACK_FILEAREA, 365 $history->id 366 ); 367 368 return format_text($feedback, $history->feedbackformat, array('context' => $context)); 369 } 370 } 371 372 /** 373 * Builds the sql and param list needed, based on the user selected filters. 374 * 375 * @return array containing sql to use and an array of params. 376 */ 377 protected function get_filters_sql_and_params() { 378 global $DB, $USER; 379 380 $coursecontext = $this->context; 381 $filter = 'gi.courseid = :courseid'; 382 $params = array( 383 'courseid' => $coursecontext->instanceid, 384 ); 385 386 if (!empty($this->filters->itemid)) { 387 $filter .= ' AND ggh.itemid = :itemid'; 388 $params['itemid'] = $this->filters->itemid; 389 } 390 if (!empty($this->filters->userids)) { 391 $list = explode(',', $this->filters->userids); 392 list($insql, $plist) = $DB->get_in_or_equal($list, SQL_PARAMS_NAMED); 393 $filter .= " AND ggh.userid $insql"; 394 $params += $plist; 395 } 396 if (!empty($this->filters->datefrom)) { 397 $filter .= " AND ggh.timemodified >= :datefrom"; 398 $params += array('datefrom' => $this->filters->datefrom); 399 } 400 if (!empty($this->filters->datetill)) { 401 $filter .= " AND ggh.timemodified <= :datetill"; 402 $params += array('datetill' => $this->filters->datetill); 403 } 404 if (!empty($this->filters->grader)) { 405 $filter .= " AND ggh.usermodified = :grader"; 406 $params += array('grader' => $this->filters->grader); 407 } 408 409 // If the course is separate group mode and the current user is not allowed to see all groups make sure 410 // that we display only users from the same groups as current user. 411 $groupmode = get_course($coursecontext->instanceid)->groupmode; 412 if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $coursecontext)) { 413 $groupids = array_column(groups_get_all_groups($coursecontext->instanceid, $USER->id, 0, 'g.id'), 'id'); 414 list($gsql, $gparams) = $DB->get_in_or_equal($groupids, SQL_PARAMS_NAMED, 'gmuparam', true, 0); 415 $filter .= " AND EXISTS (SELECT 1 FROM {groups_members} gmu WHERE gmu.userid=ggh.userid AND gmu.groupid $gsql)"; 416 $params += $gparams; 417 } 418 419 return array($filter, $params); 420 } 421 422 /** 423 * Builds the complete sql with all the joins to get the grade history data. 424 * 425 * @param bool $count setting this to true, returns an sql to get count only instead of the complete data records. 426 * 427 * @return array containing sql to use and an array of params. 428 */ 429 protected function get_sql_and_params($count = false) { 430 $fields = 'ggh.id, ggh.timemodified, ggh.itemid, ggh.userid, ggh.finalgrade, ggh.usermodified, 431 ggh.source, ggh.overridden, ggh.locked, ggh.excluded, ggh.feedback, ggh.feedbackformat, 432 gi.itemtype, gi.itemmodule, gi.iteminstance, gi.itemnumber, '; 433 434 $userfieldsapi = \core_user\fields::for_identity($this->context); 435 $userfieldssql = $userfieldsapi->get_sql('u', true, '', '', true); 436 $userfieldsselects = ''; 437 $userfieldsjoins = ''; 438 $userfieldsparams = []; 439 if (!$count) { 440 $userfieldsselects = $userfieldssql->selects; 441 $userfieldsjoins = $userfieldssql->joins; 442 $userfieldsparams = $userfieldssql->params; 443 } 444 445 // Add extra user fields that we need for the graded user. 446 $extrafields = []; 447 foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) { 448 $extrafields[$field] = $userfieldssql->mappings[$field]; 449 } 450 $userfieldsapi = \core_user\fields::for_name(); 451 $fields .= $userfieldsapi->get_sql('u', false, '', '', false)->selects . ', '; 452 $groupby = $fields; 453 454 // Add extra user fields that we need for the grader user. 455 $fields .= $userfieldsapi->get_sql('ug', false, 'grader', '', false)->selects; 456 $groupby .= $userfieldsapi->get_sql('ug', false, '', '', false)->selects; 457 458 // Filtering on revised grades only. 459 $revisedonly = !empty($this->filters->revisedonly); 460 461 if ($count && !$revisedonly) { 462 // We can only directly use count when not using the filter revised only. 463 $select = "COUNT(1)"; 464 } else { 465 // Fetching the previous grade. We use MAX() to ensure that we only get one result if 466 // more than one histories happened at the same second. 467 $prevgrade = "SELECT MAX(finalgrade) 468 FROM {grade_grades_history} h 469 WHERE h.itemid = ggh.itemid 470 AND h.userid = ggh.userid 471 AND h.timemodified < ggh.timemodified 472 AND NOT EXISTS ( 473 SELECT 1 474 FROM {grade_grades_history} h2 475 WHERE h2.itemid = ggh.itemid 476 AND h2.userid = ggh.userid 477 AND h2.timemodified < ggh.timemodified 478 AND h.timemodified < h2.timemodified)"; 479 480 $select = "$fields, ($prevgrade) AS prevgrade, 481 CASE WHEN gi.itemname IS NULL THEN gi.itemtype ELSE gi.itemname END AS itemname"; 482 } 483 484 list($where, $params) = $this->get_filters_sql_and_params(); 485 486 $sql = " SELECT $select $userfieldsselects 487 FROM {grade_grades_history} ggh 488 JOIN {grade_items} gi ON gi.id = ggh.itemid 489 JOIN {user} u ON u.id = ggh.userid 490 $userfieldsjoins 491 LEFT JOIN {user} ug ON ug.id = ggh.usermodified 492 WHERE $where"; 493 $params = array_merge($userfieldsparams, $params); 494 495 // As prevgrade is a dynamic field, we need to wrap the query. This is the only filtering 496 // that should be defined outside the method self::get_filters_sql_and_params(). 497 if ($revisedonly) { 498 $allorcount = $count ? 'COUNT(1)' : '*'; 499 $sql = "SELECT $allorcount FROM ($sql) pg 500 WHERE pg.finalgrade != pg.prevgrade 501 OR (pg.prevgrade IS NULL AND pg.finalgrade IS NOT NULL) 502 OR (pg.prevgrade IS NOT NULL AND pg.finalgrade IS NULL)"; 503 } 504 505 // Add order by if needed. 506 if (!$count && $sqlsort = $this->get_sql_sort()) { 507 $sql .= " ORDER BY " . $sqlsort; 508 } 509 510 return array($sql, $params); 511 } 512 513 /** 514 * Get the SQL fragment to sort by. 515 * 516 * This is overridden to sort by timemodified and ID by default. Many items happen at the same time 517 * and a second sorting by ID is valuable to distinguish the order in which the history happened. 518 * 519 * @return string SQL fragment. 520 */ 521 public function get_sql_sort() { 522 $columns = $this->get_sort_columns(); 523 if (count($columns) == 1 && isset($columns['timemodified']) && $columns['timemodified'] == SORT_DESC) { 524 // Add the 'id' column when we are using the default sorting. 525 $columns['id'] = SORT_DESC; 526 return self::construct_order_by($columns); 527 } 528 return parent::get_sql_sort(); 529 } 530 531 /** 532 * Query the reader. Store results in the object for use by build_table. 533 * 534 * @param int $pagesize size of page for paginated displayed table. 535 * @param bool $useinitialsbar do you want to use the initials bar. 536 */ 537 public function query_db($pagesize, $useinitialsbar = true) { 538 global $DB; 539 540 list($countsql, $countparams) = $this->get_sql_and_params(true); 541 list($sql, $params) = $this->get_sql_and_params(); 542 $total = $DB->count_records_sql($countsql, $countparams); 543 $this->pagesize($pagesize, $total); 544 if ($this->is_downloading()) { 545 $histories = $DB->get_records_sql($sql, $params); 546 } else { 547 $histories = $DB->get_records_sql($sql, $params, $this->pagesize * $this->page, $this->pagesize); 548 } 549 foreach ($histories as $history) { 550 $this->rawdata[] = $history; 551 } 552 // Set initial bars. 553 if ($useinitialsbar) { 554 $this->initialbars($total > $pagesize); 555 } 556 } 557 558 /** 559 * Returns a list of selected users. 560 * 561 * @return \stdClass[] List of user objects 562 */ 563 public function get_selected_users(): array { 564 return $this->users; 565 } 566 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body