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