See Release Notes
Long Term Support Release
<?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Vault class for a discussion list. * * @package mod_forum * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_forum\local\vaults; defined('MOODLE_INTERNAL') || die(); use core_group\output\group_details; use mod_forum\local\vaults\preprocessors\extract_record as extract_record_preprocessor; use mod_forum\local\vaults\preprocessors\extract_user as extract_user_preprocessor; use mod_forum\local\renderers\discussion_list as discussion_list_renderer; use core\dml\table as dml_table; use stdClass; /** * Discussion list vault. * * This should be the only place that accessed the database. * * This uses the repository pattern. See: * https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html * * @package mod_forum * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class discussion_list extends db_table_vault { /** The table for this vault */ private const TABLE = 'forum_discussions'; /** Alias for first author id */ private const FIRST_AUTHOR_ID_ALIAS = 'fauserpictureid'; /** Alias for author fields */ private const FIRST_AUTHOR_ALIAS = 'fauserrecord'; /** Alias for last author id */ private const LATEST_AUTHOR_ID_ALIAS = 'lauserpictureid'; /** Alias for last author fields */ private const LATEST_AUTHOR_ALIAS = 'lauserrecord'; /** Default limit */ public const PAGESIZE_DEFAULT = 100; /** Sort by newest first */ public const SORTORDER_LASTPOST_DESC = 1; /** Sort by oldest first */ public const SORTORDER_LASTPOST_ASC = 2; /** Sort by created desc */ public const SORTORDER_CREATED_DESC = 3; /** Sort by created asc */ public const SORTORDER_CREATED_ASC = 4; /** Sort by number of replies desc */ public const SORTORDER_REPLIES_DESC = 5; /** Sort by number of replies desc */ public const SORTORDER_REPLIES_ASC = 6; /** Sort by discussion name desc */ public const SORTORDER_DISCUSSION_DESC = 7; /** Sort by discussion name asc */ public const SORTORDER_DISCUSSION_ASC = 8; /** Sort by discussion starter's name desc */ public const SORTORDER_STARTER_DESC = 9; /** Sort by discussion starter's name asc */ public const SORTORDER_STARTER_ASC = 10; /** Sort by group name desc */ public const SORTORDER_GROUP_DESC = 11; /** Sort by group name asc */ public const SORTORDER_GROUP_ASC = 12; /** * Get the table alias. * * @return string */ protected function get_table_alias() : string { return 'd'; } /** * Get the favourite table alias * * @return string */ protected function get_favourite_alias() : string { return 'favalias'; } /** * Build the SQL to be used in get_records_sql. * * @param string|null $wheresql Where conditions for the SQL * @param string|null $sortsql Order by conditions for the SQL * @param int|null $userid The ID of the user we are performing this query for * * @return string */ protected function generate_get_records_sql(string $wheresql = null, ?string $sortsql = null, ?int $userid = null) : string { $alias = $this->get_table_alias(); $includefavourites = $userid ? true : false; $favsql = ''; if ($includefavourites) { list($favsql, $favparams) = $this->get_favourite_sql($userid); foreach ($favparams as $key => $param) { $favsql = str_replace(":$key", "'$param'", $favsql); } } // Fetch: // - Discussion // - First post // - Author // - Most recent editor. $thistable = new dml_table(self::TABLE, $alias, $alias); $posttable = new dml_table('forum_posts', 'fp', 'p_');< $firstauthorfields = \user_picture::fields('fa', ['deleted'], self::FIRST_AUTHOR_ID_ALIAS, self::FIRST_AUTHOR_ALIAS); < $latestuserfields = \user_picture::fields('la', ['deleted'], self::LATEST_AUTHOR_ID_ALIAS, self::LATEST_AUTHOR_ALIAS);> $userfieldsapi = \core_user\fields::for_userpic()->including('deleted'); > $firstauthorfields = $userfieldsapi->get_sql('fa', false, > self::FIRST_AUTHOR_ALIAS, self::FIRST_AUTHOR_ID_ALIAS, false)->selects; > $latestuserfields = $userfieldsapi->get_sql('la', false, > self::LATEST_AUTHOR_ALIAS, self::LATEST_AUTHOR_ID_ALIAS, false)->selects;$fields = implode(', ', [ $thistable->get_field_select(), $posttable->get_field_select(), $firstauthorfields, $latestuserfields, ]); $sortkeys = [ $this->get_sort_order(self::SORTORDER_REPLIES_DESC, $includefavourites), $this->get_sort_order(self::SORTORDER_REPLIES_ASC, $includefavourites) ]; $issortbyreplies = in_array($sortsql, $sortkeys); $tables = $thistable->get_from_sql(); $tables .= ' JOIN ' . $posttable->get_from_sql() . ' ON fp.id = ' . $alias . '.firstpost'; $tables .= ' JOIN {user} fa ON fa.id = fp.userid'; $tables .= ' JOIN {user} la ON la.id = ' . $alias . '.usermodified'; $tables .= $favsql; if ($issortbyreplies) { // Join the discussion replies. $tables .= ' JOIN ( SELECT rd.id, COUNT(rp.id) as replycount FROM {forum_discussions} rd LEFT JOIN {forum_posts} rp ON rp.discussion = rd.id AND rp.id != rd.firstpost GROUP BY rd.id ) r ON d.id = r.id'; } $groupsortorders = [ $this->get_sort_order(self::SORTORDER_GROUP_DESC, $includefavourites), $this->get_sort_order(self::SORTORDER_GROUP_ASC, $includefavourites) ]; $sortbygroup = in_array($sortsql, $groupsortorders); if ($sortbygroup) { $groupstable = new dml_table('groups', 'g', 'g'); $fields .= ', ' . $groupstable->get_field_select(); // Join groups. $tables .= 'LEFT JOIN {groups} g ON g.id = d.groupid'; } $selectsql = 'SELECT ' . $fields . ' FROM ' . $tables; $selectsql .= $wheresql ? ' WHERE ' . $wheresql : ''; $selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : ''; return $selectsql; } /** * Build the SQL to be used in count_records_sql. * * @param string|null $wheresql Where conditions for the SQL * @return string */ protected function generate_count_records_sql(string $wheresql = null) : string { $alias = $this->get_table_alias(); $db = $this->get_db(); $selectsql = "SELECT COUNT(1) FROM {" . self::TABLE . "} {$alias}"; $selectsql .= $wheresql ? ' WHERE ' . $wheresql : ''; return $selectsql; } /** * Get a list of preprocessors to execute on the DB results before being converted * into entities. * * @return array */ protected function get_preprocessors() : array { return array_merge( parent::get_preprocessors(), [ 'discussion' => new extract_record_preprocessor(self::TABLE, $this->get_table_alias()), 'firstpost' => new extract_record_preprocessor('forum_posts', 'p_'), 'firstpostauthor' => new extract_user_preprocessor(self::FIRST_AUTHOR_ID_ALIAS, self::FIRST_AUTHOR_ALIAS), 'latestpostauthor' => new extract_user_preprocessor(self::LATEST_AUTHOR_ID_ALIAS, self::LATEST_AUTHOR_ALIAS), ] ); } /** * Convert the DB records into discussion list entities. * * @param array $results The DB records * @return discussion_list[] */ protected function from_db_records(array $results) { $entityfactory = $this->get_entity_factory(); return array_map(function(array $result) use ($entityfactory) { [ 'discussion' => $discussion, 'firstpost' => $firstpost, 'firstpostauthor' => $firstpostauthor, 'latestpostauthor' => $latestpostauthor, ] = $result; return $entityfactory->get_discussion_summary_from_stdclass( $discussion, $firstpost, $firstpostauthor, $latestpostauthor ); }, $results); } /** * Get the field to sort by. * * @param int|null $sortmethod * @return string */ protected function get_keyfield(?int $sortmethod) : string { global $CFG; switch ($sortmethod) { case self::SORTORDER_CREATED_DESC: case self::SORTORDER_CREATED_ASC: return 'fp.created'; case self::SORTORDER_REPLIES_DESC: case self::SORTORDER_REPLIES_ASC: return 'replycount'; case self::SORTORDER_DISCUSSION_DESC: case self::SORTORDER_DISCUSSION_ASC: return 'dname'; case self::SORTORDER_STARTER_DESC: case self::SORTORDER_STARTER_ASC: // We'll sort by the first name field of the discussion starter's name. // Let's get the full name display config first. $nameformat = $CFG->fullnamedisplay; if ($CFG->fullnamedisplay === 'language') { $nameformat = get_string('fullnamedisplay', '', (object)['firstname' => 'firstname', 'lastname' => 'lastname']); } // Fetch all the available user name fields.< $availablefields = order_in_string(get_all_user_name_fields(), $nameformat);> $availablefields = order_in_string(\core_user\fields::get_name_fields(), $nameformat);// We'll default to the first name if there's no available name field. $returnfield = 'firstname'; if (!empty($availablefields)) { // Use the first name field. $returnfield = reset($availablefields); } return 'fauserrecord' . $returnfield; case self::SORTORDER_GROUP_DESC: case self::SORTORDER_GROUP_ASC: return 'gname'; default: global $CFG; $alias = $this->get_table_alias(); $field = "{$alias}.timemodified"; if (!empty($CFG->forum_enabletimedposts)) { return "CASE WHEN {$field} < {$alias}.timestart THEN {$alias}.timestart ELSE {$field} END"; } return $field; } } /** * Get the sort direction. * * @param int|null $sortmethod * @return string */ protected function get_sort_direction(?int $sortmethod) : string { switch ($sortmethod) { case self::SORTORDER_LASTPOST_ASC: case self::SORTORDER_CREATED_ASC: case self::SORTORDER_REPLIES_ASC: case self::SORTORDER_DISCUSSION_ASC: case self::SORTORDER_STARTER_ASC: case self::SORTORDER_GROUP_ASC: return "ASC"; case self::SORTORDER_LASTPOST_DESC: case self::SORTORDER_CREATED_DESC: case self::SORTORDER_REPLIES_DESC: case self::SORTORDER_DISCUSSION_DESC: case self::SORTORDER_STARTER_DESC: case self::SORTORDER_GROUP_DESC: default: return "DESC"; } } /** * Get the sort order SQL for a sort method. * * @param int|null $sortmethod * @param bool|null $includefavourites * @return string */ private function get_sort_order(?int $sortmethod, bool $includefavourites = true) : string { $alias = $this->get_table_alias(); // TODO consider user favourites... $keyfield = $this->get_keyfield($sortmethod); $direction = $this->get_sort_direction($sortmethod); $favouritesort = ''; if ($includefavourites) { $favalias = $this->get_favourite_alias(); // Since we're joining on the favourite table any discussion that isn't favourited will have // null in the favourite columns. Nulls behave differently in the sorting for different databases. // We can ensure consistency between databases by explicitly deprioritising any null favourite field // using a case statement. $favouritesort = ", CASE WHEN {$favalias}.id IS NULL THEN 0 ELSE 1 END DESC"; // After the null favourite fields are deprioritised and appear below the favourited discussions we // need to order the favourited discussions by id so that the most recently favourited discussions // appear at the top of the list. $favouritesort .= ", {$favalias}.itemtype DESC"; } return "{$alias}.pinned DESC $favouritesort , {$keyfield} {$direction}, {$alias}.id {$direction}"; } /** * Fetch any required SQL to respect timed posts. * * @param bool $includehiddendiscussions Whether to include hidden discussions or not * @param int|null $includepostsforuser Which user to include posts for, if any * @return array The SQL and parameters to include */ protected function get_hidden_post_sql(bool $includehiddendiscussions, ?int $includepostsforuser) { $wheresql = ''; $params = []; if (!$includehiddendiscussions) { $now = time(); $wheresql = " AND ((d.timestart <= :timestart AND (d.timeend = 0 OR d.timeend > :timeend))"; $params['timestart'] = $now; $params['timeend'] = $now; if (null !== $includepostsforuser) { $wheresql .= " OR d.userid = :byuser"; $params['byuser'] = $includepostsforuser; } $wheresql .= ")"; } return [ 'wheresql' => $wheresql, 'params' => $params, ]; } /** * Get each discussion, first post, first and last post author for the given forum, considering timed posts, and * pagination. * * @param int $forumid The forum to fetch the discussion set for * @param bool $includehiddendiscussions Whether to include hidden discussions or not * @param int|null $includepostsforuser Which user to include posts for, if any * @param int $sortorder The sort order to use * @param int $limit The number of discussions to fetch * @param int $offset The record offset * @return array The set of data fetched */ public function get_from_forum_id( int $forumid, bool $includehiddendiscussions, ?int $includepostsforuser, ?int $sortorder, int $limit, int $offset ) { $alias = $this->get_table_alias(); $wheresql = "{$alias}.forum = :forumid"; [ 'wheresql' => $hiddensql, 'params' => $hiddenparams ] = $this->get_hidden_post_sql($includehiddendiscussions, $includepostsforuser); $wheresql .= $hiddensql; $params = array_merge($hiddenparams, [ 'forumid' => $forumid, ]); $includefavourites = $includepostsforuser ? true : false; $sql = $this->generate_get_records_sql($wheresql, $this->get_sort_order($sortorder, $includefavourites), $includepostsforuser); $records = $this->get_db()->get_records_sql($sql, $params, $offset, $limit); return $this->transform_db_records_to_entities($records); } /** * Get each discussion, first post, first and last post author for the given forum, and the set of groups to display * considering timed posts, and pagination. * * @param int $forumid The forum to fetch the discussion set for * @param int[] $groupids The list of real groups to filter on * @param bool $includehiddendiscussions Whether to include hidden discussions or not * @param int|null $includepostsforuser Which user to include posts for, if any * @param int $sortorder The sort order to use * @param int $limit The number of discussions to fetch * @param int $offset The record offset * @return array The set of data fetched */ public function get_from_forum_id_and_group_id( int $forumid, array $groupids, bool $includehiddendiscussions, ?int $includepostsforuser, ?int $sortorder, int $limit, int $offset ) { $alias = $this->get_table_alias(); $wheresql = "{$alias}.forum = :forumid AND "; $groupparams = []; if (empty($groupids)) { $wheresql .= "{$alias}.groupid = :allgroupsid"; } else { list($insql, $groupparams) = $this->get_db()->get_in_or_equal($groupids, SQL_PARAMS_NAMED, 'gid'); $wheresql .= "({$alias}.groupid = :allgroupsid OR {$alias}.groupid {$insql})"; } [ 'wheresql' => $hiddensql, 'params' => $hiddenparams ] = $this->get_hidden_post_sql($includehiddendiscussions, $includepostsforuser); $wheresql .= $hiddensql; $params = array_merge($hiddenparams, $groupparams, [ 'forumid' => $forumid, 'allgroupsid' => -1, ]); $includefavourites = $includepostsforuser ? true : false; $sql = $this->generate_get_records_sql($wheresql, $this->get_sort_order($sortorder, $includefavourites), $includepostsforuser); $records = $this->get_db()->get_records_sql($sql, $params, $offset, $limit); return $this->transform_db_records_to_entities($records); } /** * Count the number of discussions in the forum. * * @param int $forumid Id of the forum to count discussions in * @param bool $includehiddendiscussions Include hidden dicussions in the count? * @param int|null $includepostsforuser Include discussions created by this user in the count * (only works if not including hidden discussions). * @return int */ public function get_total_discussion_count_from_forum_id( int $forumid, bool $includehiddendiscussions, ?int $includepostsforuser ) { $alias = $this->get_table_alias(); $wheresql = "{$alias}.forum = :forumid"; [ 'wheresql' => $hiddensql, 'params' => $hiddenparams ] = $this->get_hidden_post_sql($includehiddendiscussions, $includepostsforuser); $wheresql .= $hiddensql; $params = array_merge($hiddenparams, [ 'forumid' => $forumid, ]); return $this->get_db()->count_records_sql($this->generate_count_records_sql($wheresql), $params); } /** * Count the number of discussions in all groups and the list of groups provided. * * @param int $forumid Id of the forum to count discussions in * @param int[] $groupids List of group ids to include in the count (discussions in all groups will always be counted) * @param bool $includehiddendiscussions Include hidden dicussions in the count? * @param int|null $includepostsforuser Include discussions created by this user in the count * (only works if not including hidden discussions). * @return int */ public function get_total_discussion_count_from_forum_id_and_group_id( int $forumid, array $groupids, bool $includehiddendiscussions, ?int $includepostsforuser ) { $alias = $this->get_table_alias(); $wheresql = "{$alias}.forum = :forumid AND "; $groupparams = []; if (empty($groupids)) { $wheresql .= "{$alias}.groupid = :allgroupsid"; } else { list($insql, $groupparams) = $this->get_db()->get_in_or_equal($groupids, SQL_PARAMS_NAMED, 'gid'); $wheresql .= "({$alias}.groupid = :allgroupsid OR {$alias}.groupid {$insql})"; } [ 'wheresql' => $hiddensql, 'params' => $hiddenparams ] = $this->get_hidden_post_sql($includehiddendiscussions, $includepostsforuser); $wheresql .= $hiddensql; $params = array_merge($hiddenparams, $groupparams, [ 'forumid' => $forumid, 'allgroupsid' => -1, ]); return $this->get_db()->count_records_sql($this->generate_count_records_sql($wheresql), $params); } /** * Get the standard favouriting sql. * * @param int $userid The ID of the user we are getting the sql for * @return [$sql, $params] An array comprising of the sql and any associated params */ private function get_favourite_sql(int $userid): array { $usercontext = \context_user::instance($userid); $alias = $this->get_table_alias(); $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); list($favsql, $favparams) = $ufservice->get_join_sql_by_type('mod_forum', 'discussions', $this->get_favourite_alias(), "$alias.id"); return [$favsql, $favparams]; } }