See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 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 => $data) { 355 if (is_array($data)) { 356 foreach ($data as $key => $value) { 357 $params["{$name}[{$key}]"] = $value; 358 } 359 } else { 360 $params[$name] = $data; 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->heading(get_string('nothingtodisplay'), 4); 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 $userfields = get_extra_user_fields($this->userfieldscontext); 547 $userfieldssql = \user_picture::fields('u', $userfields); 548 549 // Define base SQL query format. 550 $this->sql->basefields = ' u.id AS userid, 551 d.course AS courseid, 552 SUM(CASE WHEN p.parent = 0 THEN 1 ELSE 0 END) AS postcount, 553 SUM(CASE WHEN p.parent != 0 THEN 1 ELSE 0 END) AS replycount, 554 ' . $userfieldssql . ', 555 SUM(CASE WHEN att.attcount IS NULL THEN 0 ELSE att.attcount END) AS attachmentcount, 556 MIN(p.created) AS earliestpost, 557 MAX(p.created) AS latestpost'; 558 559 // Handle private replies. 560 $privaterepliessql = ''; 561 $privaterepliesparams = []; 562 if (!$this->canseeprivatereplies) { 563 $privaterepliessql = ' AND (p.privatereplyto = :privatereplyto 564 OR p.userid = :privatereplyfrom 565 OR p.privatereplyto = 0)'; 566 $privaterepliesparams['privatereplyto'] = $USER->id; 567 $privaterepliesparams['privatereplyfrom'] = $USER->id; 568 } 569 570 if ($this->iscoursereport) { 571 $course = get_course($this->courseid); 572 $groupmode = groups_get_course_groupmode($course); 573 } else { 574 $cm = \cm_info::create($this->cms[0]); 575 $groupmode = $cm->effectivegroupmode; 576 } 577 578 if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $this->get_context())) { 579 $groups = groups_get_all_groups($this->courseid, $USER->id, 0, 'g.id'); 580 $groupids = array_column($groups, 'id'); 581 } else { 582 $groupids = []; 583 } 584 585 [$enrolleduserssql, $enrolledusersparams] = get_enrolled_sql($this->get_context(), '', $groupids); 586 $this->sql->params += $enrolledusersparams; 587 588 $this->sql->basefromjoins = ' {user} u 589 JOIN (' . $enrolleduserssql . ') enrolledusers ON enrolledusers.id = u.id 590 JOIN {forum} f ON f.course = :forumcourseid 591 JOIN {forum_discussions} d ON d.forum = f.id 592 LEFT JOIN {forum_posts} p ON p.discussion = d.id 593 AND p.userid = u.id 594 ' . $privaterepliessql 595 . $this->sql->filterbase['dates'] . ' 596 LEFT JOIN ( 597 SELECT COUNT(fi.id) AS attcount, fi.itemid AS postid, fi.userid 598 FROM {files} fi 599 WHERE fi.component = :component 600 AND fi.filesize > 0 601 GROUP BY fi.itemid, fi.userid 602 ) att ON att.postid = p.id 603 AND att.userid = u.id'; 604 605 $this->sql->basewhere = '1=1'; 606 607 $this->sql->basegroupby = $userfieldssql . ', d.course'; 608 609 if ($this->logreader) { 610 $this->fill_log_summary_temp_table(); 611 612 $this->sql->basefields .= ', CASE WHEN tmp.viewcount IS NOT NULL THEN tmp.viewcount ELSE 0 END AS viewcount'; 613 $this->sql->basefromjoins .= ' LEFT JOIN {' . self::LOG_SUMMARY_TEMP_TABLE . '} tmp ON tmp.userid = u.id '; 614 $this->sql->basegroupby .= ', tmp.viewcount'; 615 } 616 617 if ($this->show_word_char_counts()) { 618 // All p.wordcount values should be NOT NULL, this CASE WHEN is an extra just-in-case. 619 $this->sql->basefields .= ', SUM(CASE WHEN p.wordcount IS NOT NULL THEN p.wordcount ELSE 0 END) AS wordcount'; 620 $this->sql->basefields .= ', SUM(CASE WHEN p.charcount IS NOT NULL THEN p.charcount ELSE 0 END) AS charcount'; 621 } 622 623 $this->sql->params += [ 624 'component' => 'mod_forum', 625 'forumcourseid' => $this->courseid, 626 ] + $privaterepliesparams; 627 628 // Handle if a user is limited to viewing their own summary. 629 if (!empty($this->userid)) { 630 $this->sql->basewhere .= ' AND u.id = :userid'; 631 $this->sql->params['userid'] = $this->userid; 632 } 633 } 634 635 /** 636 * Instantiate the properties to store filter values. 637 * 638 * @return void. 639 */ 640 protected function define_base_filter_sql(): void { 641 // Filter values will be populated separately where required. 642 $this->sql->filterfields = ''; 643 $this->sql->filterfromjoins = ''; 644 $this->sql->filterwhere = ''; 645 $this->sql->filtergroupby = ''; 646 } 647 648 /** 649 * Overriding the parent method because it should not be used here. 650 * Filters are applied, so the structure of $this->sql is now different to the way this is set up in the parent. 651 * 652 * @param string $fields Unused. 653 * @param string $from Unused. 654 * @param string $where Unused. 655 * @param array $params Unused. 656 * @return void. 657 * 658 * @throws coding_exception 659 */ 660 public function set_sql($fields, $from, $where, array $params = []) { 661 throw new coding_exception('The set_sql method should not be used by the summary_table class.'); 662 } 663 664 /** 665 * Convenience method to call a number of methods for you to display the table. 666 * Overrides the parent so SQL for filters is handled. 667 * 668 * @param int $pagesize Number of rows to fetch. 669 * @param bool $useinitialsbar Whether to include the initials bar with the table. 670 * @param string $downloadhelpbutton Unused. 671 * 672 * @return void. 673 */ 674 public function out($pagesize, $useinitialsbar, $downloadhelpbutton = ''): void { 675 global $DB; 676 677 if (!$this->columns) { 678 $sql = $this->get_full_sql(); 679 680 $onerow = $DB->get_record_sql($sql, $this->sql->params, IGNORE_MULTIPLE); 681 682 // If columns is not set, define columns as the keys of the rows returned from the db. 683 $this->define_columns(array_keys((array)$onerow)); 684 $this->define_headers(array_keys((array)$onerow)); 685 } 686 687 $this->setup(); 688 $this->query_db($pagesize, $useinitialsbar); 689 $this->build_table(); 690 $this->close_recordset(); 691 $this->finish_output(); 692 693 // Drop the temp table when necessary. 694 if ($this->logreader) { 695 $this->drop_log_summary_temp_table(); 696 } 697 } 698 699 /** 700 * Apply the relevant filters to the report. 701 * 702 * @param array $filters Report filters in the format 'type' => [values]. 703 * @return void. 704 */ 705 protected function apply_filters(array $filters): void { 706 // Apply the forums filter if not reporting on every forum in a course. 707 if (!$this->accessallforums) { 708 $this->add_filter(self::FILTER_FORUM, $filters['forums']); 709 } 710 711 // Apply groups filter. 712 $this->add_filter(self::FILTER_GROUPS, $filters['groups']); 713 714 // Apply dates filter. 715 $datevalues = [ 716 'from' => $filters['datefrom'], 717 'to' => $filters['dateto'], 718 ]; 719 $this->add_filter(self::FILTER_DATES, $datevalues); 720 } 721 722 /** 723 * Prepares a complete SQL statement from the base query and any filters defined. 724 * 725 * @param bool $fullselect Whether to select all relevant columns. 726 * False selects a count only (used to calculate pagination). 727 * @return string The complete SQL statement. 728 */ 729 protected function get_full_sql(bool $fullselect = true): string { 730 $groupby = ''; 731 $orderby = ''; 732 733 if ($fullselect) { 734 $selectfields = "{$this->sql->basefields} 735 {$this->sql->filterfields}"; 736 737 $groupby = ' GROUP BY ' . $this->sql->basegroupby . $this->sql->filtergroupby; 738 739 if (($sort = $this->get_sql_sort())) { 740 $orderby = " ORDER BY {$sort}"; 741 } 742 } else { 743 $selectfields = 'COUNT(u.id)'; 744 } 745 746 $sql = "SELECT {$selectfields} 747 FROM {$this->sql->basefromjoins} 748 {$this->sql->filterfromjoins} 749 WHERE {$this->sql->basewhere} 750 {$this->sql->filterwhere} 751 {$groupby} 752 {$orderby}"; 753 754 return $sql; 755 } 756 757 /** 758 * Returns an internal and enabled log reader. 759 * 760 * @return \core\log\sql_reader|false 761 */ 762 protected function get_internal_log_reader(): ?\core\log\sql_reader { 763 global $DB; 764 765 $readers = get_log_manager()->get_readers('core\log\sql_reader'); 766 foreach ($readers as $reader) { 767 768 // If reader is not a sql_internal_table_reader and not legacy store then return. 769 if (!($reader instanceof \core\log\sql_internal_table_reader) && !($reader instanceof logstore_legacy\log\store)) { 770 continue; 771 } 772 $logreader = $reader; 773 } 774 775 if (empty($logreader)) { 776 return null; 777 } 778 779 return $logreader; 780 } 781 782 /** 783 * Fills the log summary temp table. 784 * 785 * @return null 786 */ 787 protected function fill_log_summary_temp_table() { 788 global $DB; 789 790 $this->create_log_summary_temp_table(); 791 792 if ($this->logreader instanceof logstore_legacy\log\store) { 793 $logtable = 'log'; 794 // Anonymous actions are never logged in legacy log. 795 $nonanonymous = ''; 796 } else { 797 $logtable = $this->logreader->get_internal_log_table_name(); 798 $nonanonymous = 'AND anonymous = 0'; 799 } 800 801 // Apply dates filter if applied. 802 $datewhere = $this->sql->filterbase['dateslog'] ?? ''; 803 $dateparams = $this->sql->filterbase['dateslogparams'] ?? []; 804 805 $contextids = []; 806 807 foreach ($this->forumcontexts as $forumcontext) { 808 $contextids[] = $forumcontext->id; 809 } 810 811 list($contextidin, $contextidparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); 812 813 $params = $contextidparams + $dateparams; 814 $sql = "INSERT INTO {" . self::LOG_SUMMARY_TEMP_TABLE . "} (userid, viewcount) 815 SELECT userid, COUNT(*) AS viewcount 816 FROM {" . $logtable . "} 817 WHERE contextid {$contextidin} 818 $datewhere 819 $nonanonymous 820 GROUP BY userid"; 821 $DB->execute($sql, $params); 822 } 823 824 /** 825 * Creates a temp table to store summary data from the log table for this request. 826 * 827 * @return null 828 */ 829 protected function create_log_summary_temp_table() { 830 global $DB; 831 832 $dbman = $DB->get_manager(); 833 $temptablename = self::LOG_SUMMARY_TEMP_TABLE; 834 $xmldbtable = new \xmldb_table($temptablename); 835 $xmldbtable->add_field('userid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null); 836 $xmldbtable->add_field('viewcount', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null); 837 $xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, array('userid')); 838 839 $dbman->create_temp_table($xmldbtable); 840 } 841 842 /** 843 * Drops the temp table. 844 * 845 * This should be called once the processing for the summary table has been done. 846 */ 847 protected function drop_log_summary_temp_table(): void { 848 global $DB; 849 850 // Drop the temp table if it exists. 851 $temptable = new \xmldb_table(self::LOG_SUMMARY_TEMP_TABLE); 852 $dbman = $DB->get_manager(); 853 if ($dbman->table_exists($temptable)) { 854 $dbman->drop_table($temptable); 855 } 856 } 857 858 /** 859 * Get the final list of groups to filter by, based on the groups submitted, 860 * and those the user has access to. 861 * 862 * 863 * @param array $groups The group IDs submitted. 864 * @return array Group objects of groups to use in groups filter. 865 * If no filtering required (all groups selected), returns []. 866 */ 867 protected function get_filter_groups(array $groups): array { 868 global $USER; 869 870 $usergroups = groups_get_all_groups($this->courseid, $USER->id); 871 $coursegroupsobj = groups_get_all_groups($this->courseid); 872 $allgroups = false; 873 $allowedgroupsobj = []; 874 $allowedgroups = []; 875 $filtergroups = []; 876 877 foreach ($this->cms as $cm) { 878 // Only need to check for all groups access if not confirmed by a previous check. 879 if (!$allgroups) { 880 $groupmode = groups_get_activity_groupmode($cm); 881 882 // If no groups mode enabled on the forum, nothing to prepare. 883 if (!in_array($groupmode, [VISIBLEGROUPS, SEPARATEGROUPS])) { 884 continue; 885 } 886 887 $aag = has_capability('moodle/site:accessallgroups', $this->forumcontexts[$cm->id]); 888 889 if ($groupmode == VISIBLEGROUPS || $aag) { 890 $allgroups = true; 891 892 // All groups in course fetched, no need to continue checking for others. 893 break; 894 } 895 } 896 } 897 898 if ($allgroups) { 899 $nogroups = new \stdClass(); 900 $nogroups->id = -1; 901 $nogroups->name = get_string('groupsnone'); 902 903 // Any groups and no groups. 904 $allowedgroupsobj = $coursegroupsobj + [$nogroups]; 905 } else { 906 $allowedgroupsobj = $usergroups; 907 } 908 909 foreach ($allowedgroupsobj as $group) { 910 $allowedgroups[] = $group->id; 911 } 912 913 // If not all groups in course are selected, filter by allowed groups submitted. 914 if (!empty($groups)) { 915 if (!empty(array_diff($allowedgroups, $groups))) { 916 $filtergroups = array_intersect($groups, $allowedgroups); 917 } else { 918 $coursegroups = []; 919 920 foreach ($coursegroupsobj as $group) { 921 $coursegroups[] = $group->id; 922 } 923 924 // If user's 'all groups' is a subset of the course groups, filter by all groups available to them. 925 if (!empty(array_diff($coursegroups, $allowedgroups))) { 926 $filtergroups = $allowedgroups; 927 } 928 } 929 } 930 931 return $filtergroups; 932 } 933 934 /** 935 * Download the summary report in the selected format. 936 * 937 * @param string $format The format to download the report. 938 */ 939 public function download($format) { 940 $filename = 'summary_report_' . userdate(time(), get_string('backupnameformat', 'langconfig'), 941 99, false); 942 943 $this->is_downloading($format, $filename); 944 $this->out($this->perpage, false); 945 } 946 947 /* 948 * Should the word / char counts be displayed? 949 * 950 * We don't want to show word/char columns if there is any null value because this means 951 * that they have not been calculated yet. 952 * @return bool 953 */ 954 protected function show_word_char_counts(): bool { 955 global $DB; 956 957 if (is_null($this->showwordcharcounts)) { 958 $forumids = []; 959 960 foreach ($this->cms as $cm) { 961 $forumids[] = $cm->instance; 962 } 963 964 list($forumidin, $forumidparams) = $DB->get_in_or_equal($forumids, SQL_PARAMS_NAMED); 965 966 // This should be really fast. 967 $sql = "SELECT 'x' 968 FROM {forum_posts} fp 969 JOIN {forum_discussions} fd ON fd.id = fp.discussion 970 WHERE fd.forum {$forumidin} AND (fp.wordcount IS NULL OR fp.charcount IS NULL)"; 971 972 if ($DB->record_exists_sql($sql, $forumidparams)) { 973 $this->showwordcharcounts = false; 974 } else { 975 $this->showwordcharcounts = true; 976 } 977 } 978 979 return $this->showwordcharcounts; 980 } 981 982 /** 983 * Fetch the number of items to be displayed per page. 984 * 985 * @return int 986 */ 987 public function get_perpage(): int { 988 return $this->perpage; 989 } 990 991 /** 992 * Overriding method to render the bulk actions and items per page pagination options directly below the table. 993 * 994 * @return void 995 */ 996 public function wrap_html_finish(): void { 997 global $OUTPUT; 998 999 $data = new \stdClass(); 1000 $data->showbulkactions = $this->allowbulkoperations; 1001 1002 if ($data->showbulkactions) { 1003 $data->id = 'formactionid'; 1004 $data->attributes = [ 1005 [ 1006 'name' => 'data-action', 1007 'value' => 'toggle' 1008 ], 1009 [ 1010 'name' => 'data-togglegroup', 1011 'value' => 'summaryreport-table' 1012 ], 1013 [ 1014 'name' => 'data-toggle', 1015 'value' => 'action' 1016 ], 1017 [ 1018 'name' => 'disabled', 1019 'value' => true 1020 ] 1021 ]; 1022 $data->actions = [ 1023 [ 1024 'value' => '#messageselect', 1025 'name' => get_string('messageselectadd') 1026 ] 1027 ]; 1028 } 1029 1030 // Include the pagination size selector. 1031 $perpageoptions = array_combine($this->perpageoptions, $this->perpageoptions); 1032 $selected = in_array($this->perpage, $this->perpageoptions) ? $this->perpage : $this->perpageoptions[0]; 1033 $perpageselect = new \single_select(new \moodle_url(''), 'perpage', 1034 $perpageoptions, $selected, null, 'selectperpage'); 1035 $perpageselect->set_label(get_string('perpage', 'moodle')); 1036 1037 $data->perpage = $perpageselect->export_for_template($OUTPUT); 1038 1039 echo $OUTPUT->render_from_template('forumreport_summary/bulk_action_menu', $data); 1040 } 1041 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body