See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 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 * The class for displaying the forum report table. 19 * 20 * @package forumreport_summary 21 * @copyright 2019 Michael Hawkins <michaelh@moodle.com> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace forumreport_summary; 26 defined('MOODLE_INTERNAL') || die(); 27 28 require_once($CFG->libdir . '/tablelib.php'); 29 30 use coding_exception; 31 use table_sql; 32 33 /** 34 * The class for displaying the forum report table. 35 * 36 * @copyright 2019 Michael Hawkins <michaelh@moodle.com> 37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 */ 39 class summary_table extends table_sql { 40 41 /** Forum filter type */ 42 const FILTER_FORUM = 1; 43 44 /** Groups filter type */ 45 const FILTER_GROUPS = 2; 46 47 /** Dates filter type */ 48 const FILTER_DATES = 3; 49 50 /** Table to store summary data extracted from the log table */ 51 const LOG_SUMMARY_TEMP_TABLE = 'forum_report_summary_counts'; 52 53 /** Default number of rows to display per page */ 54 const DEFAULT_PER_PAGE = 50; 55 56 /** @var \stdClass The various SQL segments that will be combined to form queries to fetch various information. */ 57 public $sql; 58 59 /** @var int The number of rows to be displayed per page. */ 60 protected $perpage = self::DEFAULT_PER_PAGE; 61 62 /** @var array The values available for pagination size per page. */ 63 protected $perpageoptions = [50, 100, 200]; 64 65 /** @var int The course ID containing the forum(s) being reported on. */ 66 protected $courseid; 67 68 /** @var bool True if reporting on all forums in course user has access to, false if reporting on a single forum */ 69 protected $iscoursereport = false; 70 71 /** @var bool True if user has access to all forums in the course (and is running course report), otherwise false. */ 72 protected $accessallforums = false; 73 74 /** @var \stdClass The course module object(s) of the forum(s) being reported on. */ 75 protected $cms = []; 76 77 /** 78 * @var int The user ID if only one user's summary will be generated. 79 * This will apply to users without permission to view others' summaries. 80 */ 81 protected $userid; 82 83 /** 84 * @var \core\log\sql_reader|null 85 */ 86 protected $logreader = null; 87 88 /** 89 * @var array of \context objects for the forums included in the report. 90 */ 91 protected $forumcontexts = []; 92 93 /** 94 * @var context_course|context_module The context where the report is being run (either a specific forum or the course). 95 */ 96 protected $userfieldscontext = null; 97 98 /** @var bool Whether the user has the capability/capabilities to perform bulk operations. */ 99 protected $allowbulkoperations = false; 100 101 /** 102 * @var bool 103 */ 104 private $showwordcharcounts = null; 105 106 /** 107 * @var bool Whether the user can see all private replies or not. 108 */ 109 protected $canseeprivatereplies; 110 111 /** 112 * @var array Validated filter data, for use in GET parameters by export links. 113 */ 114 protected $exportfilterdata = []; 115 116 /** 117 * Forum report table constructor. 118 * 119 * @param int $courseid The ID of the course the forum(s) exist within. 120 * @param array $filters Report filters in the format 'type' => [values]. 121 * @param bool $allowbulkoperations Is the user allowed to perform bulk operations? 122 * @param bool $canseeprivatereplies Whether the user can see all private replies or not. 123 * @param int $perpage The number of rows to display per page. 124 * @param bool $canexport Is the user allowed to export records? 125 * @param bool $iscoursereport Whether the user is running a course level report 126 * @param bool $accessallforums If user is running a course level report, do they have access to all forums in the course? 127 */ 128 public function __construct(int $courseid, array $filters, bool $allowbulkoperations, 129 bool $canseeprivatereplies, int $perpage, bool $canexport, bool $iscoursereport, bool $accessallforums) { 130 global $OUTPUT; 131 132 $uniqueid = $courseid . ($iscoursereport ? '' : '_' . $filters['forums'][0]); 133 parent::__construct("summaryreport_{$uniqueid}"); 134 135 $this->courseid = $courseid; 136 $this->iscoursereport = $iscoursereport; 137 $this->accessallforums = $accessallforums; 138 $this->allowbulkoperations = $allowbulkoperations; 139 $this->canseeprivatereplies = $canseeprivatereplies; 140 $this->perpage = $perpage; 141 142 $this->set_forum_properties($filters['forums']); 143 144 $columnheaders = []; 145 146 if ($allowbulkoperations) { 147 $mastercheckbox = new \core\output\checkbox_toggleall('summaryreport-table', true, [ 148 'id' => 'select-all-users', 149 'name' => 'select-all-users', 150 'label' => get_string('selectall'), 151 'labelclasses' => 'sr-only', 152 'classes' => 'm-1', 153 'checked' => false 154 ]); 155 $columnheaders['select'] = $OUTPUT->render($mastercheckbox); 156 } 157 158 $columnheaders += [ 159 'fullname' => get_string('fullnameuser'), 160 'postcount' => get_string('postcount', 'forumreport_summary'), 161 'replycount' => get_string('replycount', 'forumreport_summary'), 162 'attachmentcount' => get_string('attachmentcount', 'forumreport_summary'), 163 ]; 164 165 $this->logreader = $this->get_internal_log_reader(); 166 if ($this->logreader) { 167 $columnheaders['viewcount'] = get_string('viewcount', 'forumreport_summary'); 168 } 169 170 if ($this->show_word_char_counts()) { 171 $columnheaders['wordcount'] = get_string('wordcount', 'forumreport_summary'); 172 $columnheaders['charcount'] = get_string('charcount', 'forumreport_summary'); 173 } 174 175 $columnheaders['earliestpost'] = get_string('earliestpost', 'forumreport_summary'); 176 $columnheaders['latestpost'] = get_string('latestpost', 'forumreport_summary'); 177 178 if ($canexport) { 179 $columnheaders['export'] = get_string('exportposts', 'forumreport_summary'); 180 } 181 182 $this->define_columns(array_keys($columnheaders)); 183 $this->define_headers(array_values($columnheaders)); 184 185 // Define configs. 186 $this->define_table_configs(); 187 188 // Apply relevant filters. 189 $this->define_base_filter_sql(); 190 $this->apply_filters($filters); 191 192 // Define the basic SQL data and object format. 193 $this->define_base_sql(); 194 } 195 196 /** 197 * Sets properties that are determined by forum filter values. 198 * 199 * @param array $forumids The forum IDs passed in by the filter. 200 * @return void 201 */ 202 protected function set_forum_properties(array $forumids): void { 203 global $USER; 204 205 // Course context if reporting on all forums in the course the user has access to. 206 if ($this->iscoursereport) { 207 $this->userfieldscontext = \context_course::instance($this->courseid); 208 } 209 210 foreach ($forumids as $forumid) { 211 $cm = get_coursemodule_from_instance('forum', $forumid, $this->courseid); 212 $this->cms[] = $cm; 213 $this->forumcontexts[$cm->id] = \context_module::instance($cm->id); 214 215 // Set forum context if not reporting on course. 216 if (!isset($this->userfieldscontext)) { 217 $this->userfieldscontext = $this->forumcontexts[$cm->id]; 218 } 219 220 // Only show own summary unless they have permission to view all in every forum being reported. 221 if (empty($this->userid) && !has_capability('forumreport/summary:viewall', $this->forumcontexts[$cm->id])) { 222 $this->userid = $USER->id; 223 } 224 } 225 } 226 227 /** 228 * Provides the string name of each filter type, to be used by errors. 229 * Note: This does not use language strings as the value is injected into error strings. 230 * 231 * @param int $filtertype Type of filter 232 * @return string Name of the filter 233 */ 234 protected function get_filter_name(int $filtertype): string { 235 $filternames = [ 236 self::FILTER_FORUM => 'Forum', 237 self::FILTER_GROUPS => 'Groups', 238 self::FILTER_DATES => 'Dates', 239 ]; 240 241 return $filternames[$filtertype]; 242 } 243 244 /** 245 * Generate the select column. 246 * 247 * @param \stdClass $data 248 * @return string 249 */ 250 public function col_select($data) { 251 global $OUTPUT; 252 253 $checkbox = new \core\output\checkbox_toggleall('summaryreport-table', false, [ 254 'classes' => 'usercheckbox m-1', 255 'id' => 'user' . $data->userid, 256 'name' => 'user' . $data->userid, 257 'checked' => false, 258 'label' => get_string('selectitem', 'moodle', fullname($data)), 259 'labelclasses' => 'accesshide', 260 ]); 261 262 return $OUTPUT->render($checkbox); 263 } 264 265 /** 266 * Generate the fullname column. 267 * 268 * @param \stdClass $data The row data. 269 * @return string User's full name. 270 */ 271 public function col_fullname($data): string { 272 if ($this->is_downloading()) { 273 return fullname($data); 274 } 275 276 global $OUTPUT; 277 return $OUTPUT->user_picture($data, array('courseid' => $this->courseid, 'includefullname' => true)); 278 } 279 280 /** 281 * Generate the postcount column. 282 * 283 * @param \stdClass $data The row data. 284 * @return int number of discussion posts made by user. 285 */ 286 public function col_postcount(\stdClass $data): int { 287 return $data->postcount; 288 } 289 290 /** 291 * Generate the replycount column. 292 * 293 * @param \stdClass $data The row data. 294 * @return int number of replies made by user. 295 */ 296 public function col_replycount(\stdClass $data): int { 297 return $data->replycount; 298 } 299 300 /** 301 * Generate the attachmentcount column. 302 * 303 * @param \stdClass $data The row data. 304 * @return int number of files attached to posts by user. 305 */ 306 public function col_attachmentcount(\stdClass $data): int { 307 return $data->attachmentcount; 308 } 309 310 /** 311 * Generate the earliestpost column. 312 * 313 * @param \stdClass $data The row data. 314 * @return string Timestamp of user's earliest post, or a dash if no posts exist. 315 */ 316 public function col_earliestpost(\stdClass $data): string { 317 global $USER; 318 319 return empty($data->earliestpost) ? '-' : userdate($data->earliestpost, "", \core_date::get_user_timezone($USER)); 320 } 321 322 /** 323 * Generate the latestpost column. 324 * 325 * @param \stdClass $data The row data. 326 * @return string Timestamp of user's most recent post, or a dash if no posts exist. 327 */ 328 public function col_latestpost(\stdClass $data): string { 329 global $USER; 330 331 return empty($data->latestpost) ? '-' : userdate($data->latestpost, "", \core_date::get_user_timezone($USER)); 332 } 333 334 /** 335 * Generate the export column. 336 * 337 * @param \stdClass $data The row data. 338 * @return string The link to export content belonging to the row. 339 */ 340 public function col_export(\stdClass $data): string { 341 global $OUTPUT; 342 343 // If no posts, nothing to export. 344 if (empty($data->earliestpost)) { 345 return ''; 346 } 347 348 $params = [ 349 'id' => $this->cms[0]->instance, // Forum id. 350 'userids[]' => $data->userid, // User id. 351 ]; 352 353 // Add relevant filter params. 354 foreach ($this->exportfilterdata as $name => $filterdata) { 355 if (is_array($filterdata)) { 356 foreach ($filterdata as $key => $value) { 357 $params["{$name}[{$key}]"] = $value; 358 } 359 } else { 360 $params[$name] = $filterdata; 361 } 362 } 363 364 $buttoncontext = [ 365 'url' => new \moodle_url('/mod/forum/export.php', $params), 366 'label' => get_string('exportpostslabel', 'forumreport_summary', fullname($data)), 367 ]; 368 369 return $OUTPUT->render_from_template('forumreport_summary/export_link_button', $buttoncontext); 370 } 371 372 /** 373 * Override the default implementation to set a decent heading level. 374 * 375 * @return void. 376 */ 377 public function print_nothing_to_display(): void { 378 global $OUTPUT; 379 380 echo $OUTPUT->notification(get_string('nothingtodisplay'), \core\output\notification::NOTIFY_INFO); 381 } 382 383 /** 384 * Query the db. Store results in the table object for use by build_table. 385 * 386 * @param int $pagesize Size of page for paginated displayed table. 387 * @param bool $useinitialsbar Overridden but unused. 388 * @return void 389 */ 390 public function query_db($pagesize, $useinitialsbar = false): void { 391 global $DB; 392 393 // Set up pagination if not downloading the whole report. 394 if (!$this->is_downloading()) { 395 $totalsql = $this->get_full_sql(false); 396 397 // Set up pagination. 398 $totalrows = $DB->count_records_sql($totalsql, $this->sql->params); 399 $this->pagesize($pagesize, $totalrows); 400 } 401 402 // Fetch the data. 403 $sql = $this->get_full_sql(); 404 405 // Only paginate when not downloading. 406 if (!$this->is_downloading()) { 407 $this->rawdata = $DB->get_records_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size()); 408 } else { 409 $this->rawdata = $DB->get_records_sql($sql, $this->sql->params); 410 } 411 } 412 413 /** 414 * Adds the relevant SQL to apply a filter to the report. 415 * 416 * @param int $filtertype Filter type as defined by class constants. 417 * @param array $values Optional array of values passed into the filter type. 418 * @return void 419 * @throws coding_exception 420 */ 421 public function add_filter(int $filtertype, array $values = []): void { 422 global $DB; 423 424 $paramcounterror = false; 425 426 switch($filtertype) { 427 case self::FILTER_FORUM: 428 // Requires at least one forum ID. 429 if (empty($values)) { 430 $paramcounterror = true; 431 } else { 432 // No select fields required - displayed in title. 433 // No extra joins required, forum is already joined. 434 list($forumidin, $forumidparams) = $DB->get_in_or_equal($values, SQL_PARAMS_NAMED); 435 $this->sql->filterwhere .= " AND f.id {$forumidin}"; 436 $this->sql->params += $forumidparams; 437 } 438 439 break; 440 441 case self::FILTER_GROUPS: 442 // Filter data to only include content within specified groups (and/or no groups). 443 // Additionally, only display users who can post within the selected option(s). 444 445 // Only filter by groups the user has access to. 446 $groups = $this->get_filter_groups($values); 447 448 // Skip adding filter if not applied, or all valid options are selected. 449 if (!empty($groups)) { 450 list($groupidin, $groupidparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); 451 452 // Posts within selected groups and/or not in any groups (group ID -1) are included. 453 // No user filtering as anyone enrolled can potentially post to unrestricted discussions. 454 if (array_search(-1, $groups) !== false) { 455 $this->sql->filterwhere .= " AND d.groupid {$groupidin}"; 456 $this->sql->params += $groupidparams; 457 458 } else { 459 // Only posts and users within selected groups are included. 460 list($groupusersin, $groupusersparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); 461 462 // No joins required (handled by where to prevent data duplication). 463 $this->sql->filterwhere .= " 464 AND u.id IN ( 465 SELECT gm.userid 466 FROM {groups_members} gm 467 WHERE gm.groupid {$groupusersin} 468 ) 469 AND d.groupid {$groupidin}"; 470 $this->sql->params += $groupusersparams + $groupidparams; 471 } 472 } 473 474 break; 475 476 case self::FILTER_DATES: 477 if (!isset($values['from']['enabled']) || !isset($values['to']['enabled']) || 478 ($values['from']['enabled'] && !isset($values['from']['timestamp'])) || 479 ($values['to']['enabled'] && !isset($values['to']['timestamp']))) { 480 $paramcounterror = true; 481 } else { 482 $this->sql->filterbase['dates'] = ''; 483 $this->sql->filterbase['dateslog'] = ''; 484 $this->sql->filterbase['dateslogparams'] = []; 485 486 // From date. 487 if ($values['from']['enabled']) { 488 // If the filter was enabled, include the date restriction. 489 // Needs to form part of the base join to posts, so will be injected by define_base_sql(). 490 $this->sql->filterbase['dates'] .= " AND p.created >= :fromdate"; 491 $this->sql->params['fromdate'] = $values['from']['timestamp']; 492 $this->sql->filterbase['dateslog'] .= ' AND timecreated >= :fromdate'; 493 $this->sql->filterbase['dateslogparams']['fromdate'] = $values['from']['timestamp']; 494 $this->exportfilterdata['timestampfrom'] = $values['from']['timestamp']; 495 } 496 497 // To date. 498 if ($values['to']['enabled']) { 499 // If the filter was enabled, include the date restriction. 500 // Needs to form part of the base join to posts, so will be injected by define_base_sql(). 501 $this->sql->filterbase['dates'] .= " AND p.created <= :todate"; 502 $this->sql->params['todate'] = $values['to']['timestamp']; 503 $this->sql->filterbase['dateslog'] .= ' AND timecreated <= :todate'; 504 $this->sql->filterbase['dateslogparams']['todate'] = $values['to']['timestamp']; 505 $this->exportfilterdata['timestampto'] = $values['to']['timestamp']; 506 } 507 } 508 509 break; 510 default: 511 throw new coding_exception("Report filter type '{$filtertype}' not found."); 512 break; 513 } 514 515 if ($paramcounterror) { 516 $filtername = $this->get_filter_name($filtertype); 517 throw new coding_exception("An invalid number of values have been passed for the '{$filtername}' filter."); 518 } 519 } 520 521 /** 522 * Define various table config options. 523 * 524 * @return void. 525 */ 526 protected function define_table_configs(): void { 527 $this->collapsible(false); 528 $this->sortable(true, 'firstname', SORT_ASC); 529 $this->pageable(true); 530 $this->is_downloadable(true); 531 $this->no_sorting('select'); 532 $this->no_sorting('export'); 533 $this->set_attribute('id', 'forumreport_summary_table'); 534 $this->sql = new \stdClass(); 535 $this->sql->params = []; 536 } 537 538 /** 539 * Define the object to store all for the table SQL and initialises the base SQL required. 540 * 541 * @return void. 542 */ 543 protected function define_base_sql(): void { 544 global $USER; 545 546 // TODO Does not support custom user profile fields (MDL-70456). 547 $userfieldsapi = \core_user\fields::for_identity($this->userfieldscontext, false)->with_userpic(); 548 $userfieldssql = $userfieldsapi->get_sql('u', false, '', '', false)->selects; 549 550 // Define base SQL query format. 551 $this->sql->basefields = ' u.id AS userid, 552 d.course AS courseid, 553 SUM(CASE WHEN p.parent = 0 THEN 1 ELSE 0 END) AS postcount, 554 SUM(CASE WHEN p.parent != 0 THEN 1 ELSE 0 END) AS replycount, 555 ' . $userfieldssql . ', 556 SUM(CASE WHEN att.attcount IS NULL THEN 0 ELSE att.attcount END) AS attachmentcount, 557 MIN(p.created) AS earliestpost, 558 MAX(p.created) AS latestpost'; 559 560 // Handle private replies. 561 $privaterepliessql = ''; 562 $privaterepliesparams = []; 563 if (!$this->canseeprivatereplies) { 564 $privaterepliessql = ' AND (p.privatereplyto = :privatereplyto 565 OR p.userid = :privatereplyfrom 566 OR p.privatereplyto = 0)'; 567 $privaterepliesparams['privatereplyto'] = $USER->id; 568 $privaterepliesparams['privatereplyfrom'] = $USER->id; 569 } 570 571 if ($this->iscoursereport) { 572 $course = get_course($this->courseid); 573 $groupmode = groups_get_course_groupmode($course); 574 } else { 575 $cm = \cm_info::create($this->cms[0]); 576 $groupmode = $cm->effectivegroupmode; 577 } 578 579 if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $this->get_context())) { 580 $groups = groups_get_all_groups($this->courseid, $USER->id, 0, 'g.id'); 581 $groupids = array_column($groups, 'id'); 582 } else { 583 $groupids = []; 584 } 585 586 [$enrolleduserssql, $enrolledusersparams] = get_enrolled_sql($this->get_context(), '', $groupids); 587 $this->sql->params += $enrolledusersparams; 588 589 $queryattachments = 'SELECT COUNT(fi.id) AS attcount, fi.itemid AS postid, fi.userid 590 FROM {files} fi 591 WHERE fi.component = :component AND fi.filesize > 0 592 GROUP BY fi.itemid, fi.userid'; 593 $this->sql->basefromjoins = ' {user} u 594 JOIN (' . $enrolleduserssql . ') enrolledusers ON enrolledusers.id = u.id 595 JOIN {forum} f ON f.course = :forumcourseid 596 JOIN {forum_discussions} d ON d.forum = f.id 597 LEFT JOIN {forum_posts} p ON p.discussion = d.id AND p.userid = u.id ' 598 . $privaterepliessql 599 . $this->sql->filterbase['dates'] . ' 600 LEFT JOIN (' . $queryattachments . ') att ON att.postid = p.id AND att.userid = u.id'; 601 602 $this->sql->basewhere = '1 = 1'; 603 $this->sql->basegroupby = "$userfieldssql, d.course"; 604 605 if ($this->logreader) { 606 $this->fill_log_summary_temp_table(); 607 608 $this->sql->basefields .= ', CASE WHEN tmp.viewcount IS NOT NULL THEN tmp.viewcount ELSE 0 END AS viewcount'; 609 $this->sql->basefromjoins .= ' LEFT JOIN {' . self::LOG_SUMMARY_TEMP_TABLE . '} tmp ON tmp.userid = u.id '; 610 $this->sql->basegroupby .= ', tmp.viewcount'; 611 } 612 613 if ($this->show_word_char_counts()) { 614 // All p.wordcount values should be NOT NULL, this CASE WHEN is an extra just-in-case. 615 $this->sql->basefields .= ', SUM(CASE WHEN p.wordcount IS NOT NULL THEN p.wordcount ELSE 0 END) AS wordcount'; 616 $this->sql->basefields .= ', SUM(CASE WHEN p.charcount IS NOT NULL THEN p.charcount ELSE 0 END) AS charcount'; 617 } 618 619 $this->sql->params += [ 620 'component' => 'mod_forum', 621 'forumcourseid' => $this->courseid, 622 ] + $privaterepliesparams; 623 624 // Handle if a user is limited to viewing their own summary. 625 if (!empty($this->userid)) { 626 $this->sql->basewhere .= ' AND u.id = :userid'; 627 $this->sql->params['userid'] = $this->userid; 628 } 629 } 630 631 /** 632 * Instantiate the properties to store filter values. 633 * 634 * @return void. 635 */ 636 protected function define_base_filter_sql(): void { 637 // Filter values will be populated separately where required. 638 $this->sql->filterfields = ''; 639 $this->sql->filterfromjoins = ''; 640 $this->sql->filterwhere = ''; 641 $this->sql->filtergroupby = ''; 642 } 643 644 /** 645 * Overriding the parent method because it should not be used here. 646 * Filters are applied, so the structure of $this->sql is now different to the way this is set up in the parent. 647 * 648 * @param string $fields Unused. 649 * @param string $from Unused. 650 * @param string $where Unused. 651 * @param array $params Unused. 652 * @return void. 653 * 654 * @throws coding_exception 655 */ 656 public function set_sql($fields, $from, $where, array $params = []) { 657 throw new coding_exception('The set_sql method should not be used by the summary_table class.'); 658 } 659 660 /** 661 * Convenience method to call a number of methods for you to display the table. 662 * Overrides the parent so SQL for filters is handled. 663 * 664 * @param int $pagesize Number of rows to fetch. 665 * @param bool $useinitialsbar Whether to include the initials bar with the table. 666 * @param string $downloadhelpbutton Unused. 667 * 668 * @return void. 669 */ 670 public function out($pagesize, $useinitialsbar, $downloadhelpbutton = ''): void { 671 global $DB; 672 673 if (!$this->columns) { 674 $sql = $this->get_full_sql(); 675 676 $onerow = $DB->get_record_sql($sql, $this->sql->params, IGNORE_MULTIPLE); 677 678 // If columns is not set, define columns as the keys of the rows returned from the db. 679 $this->define_columns(array_keys((array)$onerow)); 680 $this->define_headers(array_keys((array)$onerow)); 681 } 682 683 $this->setup(); 684 $this->query_db($pagesize, $useinitialsbar); 685 $this->build_table(); 686 $this->close_recordset(); 687 $this->finish_output(); 688 689 // Drop the temp table when necessary. 690 if ($this->logreader) { 691 $this->drop_log_summary_temp_table(); 692 } 693 } 694 695 /** 696 * Apply the relevant filters to the report. 697 * 698 * @param array $filters Report filters in the format 'type' => [values]. 699 * @return void. 700 */ 701 protected function apply_filters(array $filters): void { 702 // Apply the forums filter if not reporting on every forum in a course. 703 if (!$this->accessallforums) { 704 $this->add_filter(self::FILTER_FORUM, $filters['forums']); 705 } 706 707 // Apply groups filter. 708 $this->add_filter(self::FILTER_GROUPS, $filters['groups']); 709 710 // Apply dates filter. 711 $datevalues = [ 712 'from' => $filters['datefrom'], 713 'to' => $filters['dateto'], 714 ]; 715 $this->add_filter(self::FILTER_DATES, $datevalues); 716 } 717 718 /** 719 * Prepares a complete SQL statement from the base query and any filters defined. 720 * 721 * @param bool $fullselect Whether to select all relevant columns. 722 * False selects a count only (used to calculate pagination). 723 * @return string The complete SQL statement. 724 */ 725 protected function get_full_sql(bool $fullselect = true): string { 726 $groupby = ''; 727 $orderby = ''; 728 729 if ($fullselect) { 730 $selectfields = "{$this->sql->basefields} 731 {$this->sql->filterfields}"; 732 733 $groupby = ' GROUP BY ' . $this->sql->basegroupby . $this->sql->filtergroupby; 734 735 if ($sort = $this->get_sql_sort()) { 736 $orderby = " ORDER BY {$sort}"; 737 } 738 } else { 739 $selectfields = 'COUNT(u.id)'; 740 } 741 742 $sql = "SELECT {$selectfields} 743 FROM {$this->sql->basefromjoins} 744 {$this->sql->filterfromjoins} 745 WHERE {$this->sql->basewhere} 746 {$this->sql->filterwhere} 747 {$groupby} 748 {$orderby}"; 749 750 return $sql; 751 } 752 753 /** 754 * Returns an internal and enabled log reader. 755 * 756 * @return \core\log\sql_reader|false 757 */ 758 protected function get_internal_log_reader(): ?\core\log\sql_reader { 759 global $DB; 760 761 $readers = get_log_manager()->get_readers('core\log\sql_reader'); 762 foreach ($readers as $reader) { 763 764 // If reader is not a sql_internal_table_reader and not legacy store then return. 765 if (!($reader instanceof \core\log\sql_internal_table_reader) && !($reader instanceof logstore_legacy\log\store)) { 766 continue; 767 } 768 $logreader = $reader; 769 } 770 771 if (empty($logreader)) { 772 return null; 773 } 774 775 return $logreader; 776 } 777 778 /** 779 * Fills the log summary temp table. 780 * 781 * @return null 782 */ 783 protected function fill_log_summary_temp_table() { 784 global $DB; 785 786 $this->create_log_summary_temp_table(); 787 788 if ($this->logreader instanceof logstore_legacy\log\store) { 789 $logtable = 'log'; 790 // Anonymous actions are never logged in legacy log. 791 $nonanonymous = ''; 792 } else { 793 $logtable = $this->logreader->get_internal_log_table_name(); 794 $nonanonymous = 'AND anonymous = 0'; 795 } 796 797 // Apply dates filter if applied. 798 $datewhere = $this->sql->filterbase['dateslog'] ?? ''; 799 $dateparams = $this->sql->filterbase['dateslogparams'] ?? []; 800 801 $contextids = []; 802 803 foreach ($this->forumcontexts as $forumcontext) { 804 $contextids[] = $forumcontext->id; 805 } 806 807 list($contextidin, $contextidparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); 808 809 $params = $contextidparams + $dateparams; 810 $sql = "INSERT INTO {" . self::LOG_SUMMARY_TEMP_TABLE . "} (userid, viewcount) 811 SELECT userid, COUNT(*) AS viewcount 812 FROM {" . $logtable . "} 813 WHERE contextid {$contextidin} 814 $datewhere 815 $nonanonymous 816 GROUP BY userid"; 817 $DB->execute($sql, $params); 818 } 819 820 /** 821 * Creates a temp table to store summary data from the log table for this request. 822 * 823 * @return null 824 */ 825 protected function create_log_summary_temp_table() { 826 global $DB; 827 828 $dbman = $DB->get_manager(); 829 $temptablename = self::LOG_SUMMARY_TEMP_TABLE; 830 $xmldbtable = new \xmldb_table($temptablename); 831 $xmldbtable->add_field('userid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null); 832 $xmldbtable->add_field('viewcount', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null); 833 $xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, array('userid')); 834 835 $dbman->create_temp_table($xmldbtable); 836 } 837 838 /** 839 * Drops the temp table. 840 * 841 * This should be called once the processing for the summary table has been done. 842 */ 843 protected function drop_log_summary_temp_table(): void { 844 global $DB; 845 846 // Drop the temp table if it exists. 847 $temptable = new \xmldb_table(self::LOG_SUMMARY_TEMP_TABLE); 848 $dbman = $DB->get_manager(); 849 if ($dbman->table_exists($temptable)) { 850 $dbman->drop_table($temptable); 851 } 852 } 853 854 /** 855 * Get the final list of groups to filter by, based on the groups submitted, 856 * and those the user has access to. 857 * 858 * 859 * @param array $groups The group IDs submitted. 860 * @return array Group objects of groups to use in groups filter. 861 * If no filtering required (all groups selected), returns []. 862 */ 863 protected function get_filter_groups(array $groups): array { 864 global $USER; 865 866 $usergroups = groups_get_all_groups($this->courseid, $USER->id); 867 $coursegroupsobj = groups_get_all_groups($this->courseid); 868 $allgroups = false; 869 $allowedgroupsobj = []; 870 $allowedgroups = []; 871 $filtergroups = []; 872 873 foreach ($this->cms as $cm) { 874 // Only need to check for all groups access if not confirmed by a previous check. 875 if (!$allgroups) { 876 $groupmode = groups_get_activity_groupmode($cm); 877 878 // If no groups mode enabled on the forum, nothing to prepare. 879 if (!in_array($groupmode, [VISIBLEGROUPS, SEPARATEGROUPS])) { 880 continue; 881 } 882 883 $aag = has_capability('moodle/site:accessallgroups', $this->forumcontexts[$cm->id]); 884 885 if ($groupmode == VISIBLEGROUPS || $aag) { 886 $allgroups = true; 887 888 // All groups in course fetched, no need to continue checking for others. 889 break; 890 } 891 } 892 } 893 894 if ($allgroups) { 895 $nogroups = new \stdClass(); 896 $nogroups->id = -1; 897 $nogroups->name = get_string('groupsnone'); 898 899 // Any groups and no groups. 900 $allowedgroupsobj = $coursegroupsobj + [$nogroups]; 901 } else { 902 $allowedgroupsobj = $usergroups; 903 } 904 905 foreach ($allowedgroupsobj as $group) { 906 $allowedgroups[] = $group->id; 907 } 908 909 // If not all groups in course are selected, filter by allowed groups submitted. 910 if (!empty($groups)) { 911 if (!empty(array_diff($allowedgroups, $groups))) { 912 $filtergroups = array_intersect($groups, $allowedgroups); 913 } else { 914 $coursegroups = []; 915 916 foreach ($coursegroupsobj as $group) { 917 $coursegroups[] = $group->id; 918 } 919 920 // If user's 'all groups' is a subset of the course groups, filter by all groups available to them. 921 if (!empty(array_diff($coursegroups, $allowedgroups))) { 922 $filtergroups = $allowedgroups; 923 } 924 } 925 } 926 927 return $filtergroups; 928 } 929 930 /** 931 * Download the summary report in the selected format. 932 * 933 * @param string $format The format to download the report. 934 */ 935 public function download($format) { 936 $filename = 'summary_report_' . userdate(time(), get_string('backupnameformat', 'langconfig'), 937 99, false); 938 939 $this->is_downloading($format, $filename); 940 $this->out($this->perpage, false); 941 } 942 943 /* 944 * Should the word / char counts be displayed? 945 * 946 * We don't want to show word/char columns if there is any null value because this means 947 * that they have not been calculated yet. 948 * @return bool 949 */ 950 protected function show_word_char_counts(): bool { 951 global $DB; 952 953 if (is_null($this->showwordcharcounts)) { 954 $forumids = []; 955 956 foreach ($this->cms as $cm) { 957 $forumids[] = $cm->instance; 958 } 959 960 list($forumidin, $forumidparams) = $DB->get_in_or_equal($forumids, SQL_PARAMS_NAMED); 961 962 // This should be really fast. 963 $sql = "SELECT 'x' 964 FROM {forum_posts} fp 965 JOIN {forum_discussions} fd ON fd.id = fp.discussion 966 WHERE fd.forum {$forumidin} AND (fp.wordcount IS NULL OR fp.charcount IS NULL)"; 967 968 if ($DB->record_exists_sql($sql, $forumidparams)) { 969 $this->showwordcharcounts = false; 970 } else { 971 $this->showwordcharcounts = true; 972 } 973 } 974 975 return $this->showwordcharcounts; 976 } 977 978 /** 979 * Fetch the number of items to be displayed per page. 980 * 981 * @return int 982 */ 983 public function get_perpage(): int { 984 return $this->perpage; 985 } 986 987 /** 988 * Overriding method to render the bulk actions and items per page pagination options directly below the table. 989 * 990 * @return void 991 */ 992 public function wrap_html_finish(): void { 993 global $OUTPUT; 994 995 $data = new \stdClass(); 996 $data->showbulkactions = $this->allowbulkoperations; 997 998 if ($data->showbulkactions) { 999 $data->id = 'formactionid'; 1000 $data->attributes = [ 1001 [ 1002 'name' => 'data-action', 1003 'value' => 'toggle' 1004 ], 1005 [ 1006 'name' => 'data-togglegroup', 1007 'value' => 'summaryreport-table' 1008 ], 1009 [ 1010 'name' => 'data-toggle', 1011 'value' => 'action' 1012 ], 1013 [ 1014 'name' => 'disabled', 1015 'value' => true 1016 ] 1017 ]; 1018 $data->actions = [ 1019 [ 1020 'value' => '#messageselect', 1021 'name' => get_string('messageselectadd') 1022 ] 1023 ]; 1024 } 1025 1026 // Include the pagination size selector. 1027 $perpageoptions = array_combine($this->perpageoptions, $this->perpageoptions); 1028 $selected = in_array($this->perpage, $this->perpageoptions) ? $this->perpage : $this->perpageoptions[0]; 1029 $perpageselect = new \single_select(new \moodle_url(''), 'perpage', 1030 $perpageoptions, $selected, null, 'selectperpage'); 1031 $perpageselect->set_label(get_string('perpage', 'moodle')); 1032 1033 $data->perpage = $perpageselect->export_for_template($OUTPUT); 1034 1035 echo $OUTPUT->render_from_template('forumreport_summary/bulk_action_menu', $data); 1036 } 1037 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body