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