From 6388b6572c967e2eb7cfdf2c2a3f16a7f90caa87 Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Fri, 19 Jul 2019 12:29:11 +0800 Subject: [PATCH] MDL-66153 mod_forum: Introduced summary report sub-plugin Part of MDL-66076. --- mod/forum/classes/plugininfo/forumreport.php | 45 +++ mod/forum/db/subplugins.json | 5 + mod/forum/lib.php | 18 +- .../summary/classes/privacy/provider.php | 46 +++ .../report/summary/classes/summary_table.php | 364 ++++++++++++++++++ mod/forum/report/summary/db/access.php | 49 +++ mod/forum/report/summary/index.php | 79 ++++ .../summary/lang/en/forumreport_summary.php | 32 ++ mod/forum/report/summary/version.php | 29 ++ mod/forum/version.php | 2 +- 10 files changed, 667 insertions(+), 2 deletions(-) create mode 100644 mod/forum/classes/plugininfo/forumreport.php create mode 100644 mod/forum/db/subplugins.json create mode 100644 mod/forum/report/summary/classes/privacy/provider.php create mode 100644 mod/forum/report/summary/classes/summary_table.php create mode 100644 mod/forum/report/summary/db/access.php create mode 100644 mod/forum/report/summary/index.php create mode 100644 mod/forum/report/summary/lang/en/forumreport_summary.php create mode 100644 mod/forum/report/summary/version.php diff --git a/mod/forum/classes/plugininfo/forumreport.php b/mod/forum/classes/plugininfo/forumreport.php new file mode 100644 index 00000000000..a85ed0ec16f --- /dev/null +++ b/mod/forum/classes/plugininfo/forumreport.php @@ -0,0 +1,45 @@ +. + +/** + * Subplugin info class. + * + * @package mod_forum + * @copyright 2019 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_forum\plugininfo; + +use core\plugininfo\base; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Forum report subplugin info class. + * + * @copyright 2019 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class forumreport extends base { + /** + * Allow the forum report subplugin be uninstalled. + * + * @return boolean + */ + public function is_uninstall_allowed() { + return true; + } +} diff --git a/mod/forum/db/subplugins.json b/mod/forum/db/subplugins.json new file mode 100644 index 00000000000..d81f1fc6499 --- /dev/null +++ b/mod/forum/db/subplugins.json @@ -0,0 +1,5 @@ +{ + "plugintypes": { + "forumreport": "mod\/forum\/report" + } +} diff --git a/mod/forum/lib.php b/mod/forum/lib.php index b8c3939e8ae..db0eb65cfb0 100644 --- a/mod/forum/lib.php +++ b/mod/forum/lib.php @@ -5251,7 +5251,23 @@ function forum_extend_settings_navigation(settings_navigation $settingsnav, navi $discussionid = $params['d']; } - // for some actions you need to be enrolled, beiing admin is not enough sometimes here + // Display all forum reports user has access to. + if (isloggedin() && !isguestuser()) { + $reportnames = array_keys(core_component::get_plugin_list('forumreport')); + + foreach ($reportnames as $reportname) { + if (has_capability("forumreport/{$reportname}:view", $PAGE->cm->context)) { + $reportlinkparams = [ + 'courseid' => $forumobject->course, + 'forumid' => $forumobject->id, + ]; + $reportlink = new moodle_url("/mod/forum/report/{$reportname}/index.php", $reportlinkparams); + $forumnode->add(get_string('nodetitle', "forumreport_{$reportname}"), $reportlink, navigation_node::TYPE_CONTAINER); + } + } + } + + // For some actions you need to be enrolled, being admin is not enough sometimes here. $enrolled = is_enrolled($PAGE->cm->context, $USER, '', false); $activeenrolled = is_enrolled($PAGE->cm->context, $USER, '', true); diff --git a/mod/forum/report/summary/classes/privacy/provider.php b/mod/forum/report/summary/classes/privacy/provider.php new file mode 100644 index 00000000000..5670f15fec1 --- /dev/null +++ b/mod/forum/report/summary/classes/privacy/provider.php @@ -0,0 +1,46 @@ +. + +/** + * Privacy Subsystem implementation for forumreport_summary subplugin. + * + * @package forumreport_summary + * @copyright 2019 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace forumreport_summary\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem implementation for forumreport_summary subplugin, implementing null_provider. + * + * @copyright 2019 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason() : string { + return 'privacy:metadata'; + } +} diff --git a/mod/forum/report/summary/classes/summary_table.php b/mod/forum/report/summary/classes/summary_table.php new file mode 100644 index 00000000000..bef374af687 --- /dev/null +++ b/mod/forum/report/summary/classes/summary_table.php @@ -0,0 +1,364 @@ +. + +/** + * The class for displaying the forum report table. + * + * @package forumreport_summary + * @copyright 2019 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace forumreport_summary; +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/tablelib.php'); + +use coding_exception; +use table_sql; + +/** + * The class for displaying the forum report table. + * + * @copyright 2019 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class summary_table extends table_sql { + + /** Forum filter type */ + const FILTER_FORUM = 1; + + /** @var \stdClass The various SQL segments that will be combined to form queries to fetch various information. */ + public $sql; + + /** @var int The number of rows to be displayed per page. */ + protected $perpage = 25; + + /** @var int The course ID being reported on. */ + protected $courseid; + + /** @var int The forum ID being reported on. */ + protected $forumid; + + /** + * @var int The user ID if only one user's summary will be generated. + * This will apply to users without permission to view others' summaries. + */ + protected $userid; + + /** + * Forum report table constructor. + * + * @param int $courseid The ID of the course the forum(s) exist within. + * @param int $forumid The ID of the forum being summarised. + */ + public function __construct(int $courseid, int $forumid) { + global $USER; + + parent::__construct("summaryreport_{$courseid}_{$forumid}"); + + $cm = get_coursemodule_from_instance('forum', $forumid, $courseid); + $context = \context_module::instance($cm->id); + + // Only show their own summary unless they have permission to view all. + if (!has_capability('forumreport/summary:viewall', $context)) { + $this->userid = $USER->id; + } + + $this->courseid = intval($courseid); + + $columnheaders = [ + 'fullname' => get_string('fullnameuser'), + 'postcount' => get_string('postcount', 'forumreport_summary'), + 'replycount' => get_string('replycount', 'forumreport_summary'), + ]; + + $this->define_columns(array_keys($columnheaders)); + $this->define_headers(array_values($columnheaders)); + + // Define configs. + $this->define_table_configs(); + + // Define the basic SQL data and object format. + $this->define_base_sql(); + + // Set the forum ID. + $this->add_filter(self::FILTER_FORUM, [$forumid]); + } + + /** + * Provides the string name of each filter type. + * + * @param int $filtertype Type of filter + * @return string Name of the filter + */ + public function get_filter_name(int $filtertype): string { + $filternames = [ + self::FILTER_FORUM => 'Forum', + ]; + + return $filternames[$filtertype]; + } + + /** + * Generate the fullname column. + * + * @param \stdClass $data The row data. + * @return string User's full name. + */ + public function col_fullname($data): string { + $fullname = $data->firstname . ' ' . $data->lastname; + + return $fullname; + } + + /** + * Generate the postcount column. + * + * @param \stdClass $data The row data. + * @return int number of discussion posts made by user. + */ + public function col_postcount(\stdClass $data): int { + return $data->postcount; + } + + /** + * Generate the replycount column. + * + * @param \stdClass $data The row data. + * @return int number of replies made by user. + */ + public function col_replycount(\stdClass $data): int { + return $data->replycount; + } + + /** + * Override the default implementation to set a decent heading level. + * + * @return void. + */ + public function print_nothing_to_display(): void { + global $OUTPUT; + + echo $OUTPUT->heading(get_string('nothingtodisplay'), 4); + } + + /** + * Query the db. Store results in the table object for use by build_table. + * + * @param int $pagesize Size of page for paginated displayed table. + * @param bool $useinitialsbar Overridden but unused. + * @return void + */ + public function query_db($pagesize, $useinitialsbar = false): void { + global $DB; + + // Set up pagination if not downloading the whole report. + if (!$this->is_downloading()) { + $totalsql = $this->get_full_sql(false); + + // Set up pagination. + $totalrows = $DB->count_records_sql($totalsql, $this->sql->params); + $this->pagesize($pagesize, $totalrows); + } + + // Fetch the data. + $sql = $this->get_full_sql(); + + // Only paginate when not downloading. + if (!$this->is_downloading()) { + $this->rawdata = $DB->get_records_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size()); + } else { + $this->rawdata = $DB->get_records_sql($sql, $this->sql->params); + } + } + + /** + * Adds the relevant SQL to apply a filter to the report. + * + * @param int $filtertype Filter type as defined by class constants. + * @param array $values Optional array of values passed into the filter type. + * @return void + * @throws coding_exception + */ + public function add_filter(int $filtertype, array $values = []): void { + $paramcounterror = false; + + switch($filtertype) { + case self::FILTER_FORUM: + // Requires exactly one forum ID. + if (count($values) != 1) { + $paramcounterror = true; + } else { + // No select fields required - displayed in title. + // No extra joins required, forum is already joined. + $this->sql->filterwhere .= ' AND f.id = :forumid'; + $this->sql->params['forumid'] = $values[0]; + } + + break; + + default: + throw new coding_exception("Report filter type '{$filtertype}' not found."); + break; + } + + if ($paramcounterror) { + $filtername = $this->get_filter_name($filtertype); + throw new coding_exception("An invalid number of values have been passed for the '{$filtername}' filter."); + } + } + + /** + * Define various table config options. + * + * @return void. + */ + protected function define_table_configs(): void { + $this->collapsible(false); + $this->sortable(true, 'firstname', SORT_ASC); + $this->pageable(true); + $this->no_sorting('select'); + } + + /** + * Define the object to store all for the table SQL and initialises the base SQL required. + * + * @return void. + */ + protected function define_base_sql(): void { + $this->sql = new \stdClass(); + + // Define base SQL query format. + // Ignores private replies as they are not visible to all participants. + $this->sql->basefields = ' ue.userid AS userid, + e.courseid AS courseid, + f.id as forumid, + SUM(CASE WHEN p.parent = 0 THEN 1 ELSE 0 END) AS postcount, + SUM(CASE WHEN p.parent != 0 THEN 1 ELSE 0 END) AS replycount, + u.firstname, + u.lastname'; + + $this->sql->basefromjoins = ' {enrol} e + JOIN {user_enrolments} ue ON ue.enrolid = e.id + JOIN {user} u ON u.id = ue.userid + JOIN {forum} f ON f.course = e.courseid + JOIN {forum_discussions} d ON d.forum = f.id + LEFT JOIN {forum_posts} p ON p.discussion = d.id + AND p.userid = ue.userid + AND p.privatereplyto = 0'; + + $this->sql->basewhere = 'e.courseid = :courseid'; + + $this->sql->basegroupby = 'ue.userid, e.courseid, f.id, u.firstname, u.lastname'; + + $this->sql->params = ['courseid' => $this->courseid]; + + // Handle if a user is limited to viewing their own summary. + if (!empty($this->userid)) { + $this->sql->basewhere .= ' AND ue.userid = :userid'; + $this->sql->params['userid'] = $this->userid; + } + + // Filter values will be populated separately where required. + $this->sql->filterfields = ''; + $this->sql->filterfromjoins = ''; + $this->sql->filterwhere = ''; + $this->sql->filtergroupby = ''; + } + + /** + * Overriding the parent method because it should not be used here. + * Filters are applied, so the structure of $this->sql is now different to the way this is set up in the parent. + * + * @param string $fields Unused. + * @param string $from Unused. + * @param string $where Unused. + * @param array $params Unused. + * @return void. + * + * @throws coding_exception + */ + public function set_sql($fields, $from, $where, array $params = []) { + throw new coding_exception('The set_sql method should not be used by the summary_table class.'); + } + + /** + * Convenience method to call a number of methods for you to display the table. + * Overrides the parent so SQL for filters is handled. + * + * @param int $pagesize Number of rows to fetch. + * @param bool $useinitialsbar Whether to include the initials bar with the table. + * @param string $downloadhelpbutton Unused. + * + * @return void. + */ + public function out($pagesize, $useinitialsbar, $downloadhelpbutton = ''): void { + global $DB; + + if (!$this->columns) { + $sql = $this->get_full_sql(); + + $onerow = $DB->get_record_sql($sql, $this->sql->params, IGNORE_MULTIPLE); + + // If columns is not set, define columns as the keys of the rows returned from the db. + $this->define_columns(array_keys((array)$onerow)); + $this->define_headers(array_keys((array)$onerow)); + } + + $this->setup(); + $this->query_db($pagesize, $useinitialsbar); + $this->build_table(); + $this->close_recordset(); + $this->finish_output(); + } + + /** + * Prepares a complete SQL statement from the base query and any filters defined. + * + * @param bool $fullselect Whether to select all relevant columns. + * False selects a count only (used to calculate pagination). + * @return string The complete SQL statement. + */ + protected function get_full_sql(bool $fullselect = true): string { + $selectfields = ''; + $groupby = ''; + $orderby = ''; + + if ($fullselect) { + $selectfields = "{$this->sql->basefields} + {$this->sql->filterfields}"; + + $groupby = ' GROUP BY ' . $this->sql->basegroupby . $this->sql->filtergroupby; + + if (($sort = $this->get_sql_sort())) { + $orderby = " ORDER BY {$sort}"; + } + } else { + $selectfields = 'COUNT(DISTINCT(ue.userid))'; + } + + $sql = "SELECT {$selectfields} + FROM {$this->sql->basefromjoins} + {$this->sql->filterfromjoins} + WHERE {$this->sql->basewhere} + {$this->sql->filterwhere} + {$groupby} + {$orderby}"; + + return $sql; + } +} diff --git a/mod/forum/report/summary/db/access.php b/mod/forum/report/summary/db/access.php new file mode 100644 index 00000000000..e1c4636a772 --- /dev/null +++ b/mod/forum/report/summary/db/access.php @@ -0,0 +1,49 @@ +. + +/** + * Forum summary report subplugin capabilities. + * + * @package forumreport_summary + * @copyright 2019 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = [ + 'forumreport/summary:view' => [ + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ], + ], + + 'forumreport/summary:viewall' => [ + 'riskbitmask' => RISK_PERSONAL, + + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ], + ], +]; diff --git a/mod/forum/report/summary/index.php b/mod/forum/report/summary/index.php new file mode 100644 index 00000000000..0a898e92441 --- /dev/null +++ b/mod/forum/report/summary/index.php @@ -0,0 +1,79 @@ +. + +/** + * This script displays the forum summary report for the given parameters, within a user's capabilities. + * + * @package forumreport_summary + * @copyright 2019 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once("../../../../config.php"); + +if (isguestuser()) { + print_error('noguest'); +} + +$courseid = required_param('courseid', PARAM_INT); +$forumid = required_param('forumid', PARAM_INT); +$perpage = optional_param('perpage', 25, PARAM_INT); +$cm = null; +$modinfo = get_fast_modinfo($courseid); + +if ($forumid > 0) { + if (!isset($modinfo->instances['forum'][$forumid])) { + throw new \moodle_exception("A valid forum ID is required to generate a summary report."); + } + + $foruminfo = $modinfo->instances['forum'][$forumid]; + $forumname = $foruminfo->name; + $cm = $foruminfo->get_course_module_record(); +} else { + throw new \moodle_exception("A valid forum ID is required to generate a summary report."); +} + +require_login($courseid, false, $cm); + +// This capability is required to view any version of the report. +$context = \context_module::instance($cm->id); +if (!has_capability("forumreport/summary:view", $context)) { + $redirecturl = new moodle_url("/mod/forum/view.php"); + $redirecturl->param('id', $forumid); + redirect($redirecturl); +} + +$course = $modinfo->get_course(); + +$urlparams = [ + 'courseid' => $courseid, + 'forumid' => $forumid, + 'perpage' => $perpage, +]; +$url = new moodle_url("/mod/forum/report/summary/index.php", $urlparams); + +$PAGE->set_url($url); +$PAGE->set_pagelayout('report'); +$PAGE->set_title($forumname); +$PAGE->set_heading($course->fullname); + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('summarytitle', 'forumreport_summary', $forumname), 2, 'p-b-2'); + +$table = new \forumreport_summary\summary_table($courseid, $forumid); +$table->baseurl = $url; +$table->out($perpage, false); +echo $OUTPUT->footer(); diff --git a/mod/forum/report/summary/lang/en/forumreport_summary.php b/mod/forum/report/summary/lang/en/forumreport_summary.php new file mode 100644 index 00000000000..4d140786cb6 --- /dev/null +++ b/mod/forum/report/summary/lang/en/forumreport_summary.php @@ -0,0 +1,32 @@ +. + +/** + * Strings for the Forum report subplugin. + * + * @package forumreport_summary + * @copyright 2019 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['nodetitle'] = 'Summary report'; +$string['pluginname'] = 'Forum summary report'; +$string['postcount'] = 'Number of discussions posted'; +$string['privacy:metadata'] = 'The Forum summary report plugin does not store any personal data.'; +$string['replycount'] = 'Number of replies posted'; +$string['summary:viewall'] = 'Access summary report data for each user within a given forum or forums'; +$string['summary:view'] = 'Access summary report within a given forum or forums'; +$string['summarytitle'] = 'Summary report - {$a}'; diff --git a/mod/forum/report/summary/version.php b/mod/forum/report/summary/version.php new file mode 100644 index 00000000000..7ff17f7e1d1 --- /dev/null +++ b/mod/forum/report/summary/version.php @@ -0,0 +1,29 @@ +. + +/** + * Defines the version of the forum report subplugin. + * + * @package forumreport_summary + * @copyright 2019 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2019082000; +$plugin->requires = 2019071900; +$plugin->component = 'forumreport_summary'; diff --git a/mod/forum/version.php b/mod/forum/version.php index 2777c1ffea8..1a5da4cd321 100644 --- a/mod/forum/version.php +++ b/mod/forum/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2019052001; // The current module version (Date: YYYYMMDDXX) +$plugin->version = 2019071900; // The current module version (Date: YYYYMMDDXX) $plugin->requires = 2019051100; // Requires this Moodle version $plugin->component = 'mod_forum'; // Full name of the plugin (used for diagnostics)