first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,155 @@
<?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/>.
/**
* Exported discussion builder class.
*
* @package mod_forum
* @copyright 2019 Peter Dias<peter@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\builders;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\vault as vault_factory;
use rating_manager;
use renderer_base;
use stdClass;
/**
* Exported discussion builder class
*
* This class is an implementation of the builder pattern (loosely). It is responsible
* for taking a set of related forums, discussions, and posts and generate the exported
* version of the discussion.
*
* It encapsulates the complexity involved with exporting discussions. All of the relevant
* additional resources will be loaded by this class in order to ensure the exporting
* process can happen.
*
* See this doc for more information on the builder pattern:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/Builder/README.html
*
* @copyright 2019 Peter Dias<peter@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class exported_discussion {
/** @var renderer_base $renderer Core renderer */
private $renderer;
/** @var legacy_data_mapper_factory $legacydatamapperfactory Data mapper factory */
private $legacydatamapperfactory;
/** @var exporter_factory $exporterfactory Exporter factory */
private $exporterfactory;
/** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory;
/** @var rating_manager $ratingmanager Rating manager */
private $ratingmanager;
/**
* Constructor.
*
* @param renderer_base $renderer Core renderer
* @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
* @param exporter_factory $exporterfactory Exporter factory
* @param vault_factory $vaultfactory Vault factory
* @param rating_manager $ratingmanager Rating manager
*/
public function __construct(
renderer_base $renderer,
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
rating_manager $ratingmanager
) {
$this->renderer = $renderer;
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->ratingmanager = $ratingmanager;
}
/**
* Build any additional variables for the exported discussion for a given set of discussions.
*
* This will typically be used for a list of discussions in the same forum.
*
* @param stdClass $user The user to export the posts for.
* @param forum_entity $forum The forum that each of the $discussions belong to
* @param discussion_entity $discussion A list of all discussions that each of the $posts belong to
* @return stdClass[] List of exported posts in the same order as the $posts array.
*/
public function build(
stdClass $user,
forum_entity $forum,
discussion_entity $discussion
): array {
$favouriteids = [];
if ($this->is_favourited($discussion, $forum->get_context(), $user)) {
$favouriteids[] = $discussion->get_id();
}
$groupsbyid = $this->get_groups_available_in_forum($forum);
$discussionexporter = $this->exporterfactory->get_discussion_exporter(
$user, $forum, $discussion, $groupsbyid, $favouriteids
);
return (array) $discussionexporter->export($this->renderer);
}
/**
* Get the groups details for all groups available to the forum.
* @param forum_entity $forum The forum entity
* @return stdClass[]
*/
private function get_groups_available_in_forum($forum): array {
$course = $forum->get_course_record();
$coursemodule = $forum->get_course_module_record();
return groups_get_all_groups($course->id, 0, $coursemodule->groupingid);
}
/**
* Check whether the provided discussion has been favourited by the user.
*
* @param discussion_entity $discussion The discussion record
* @param \context_module $forumcontext Forum context
* @param \stdClass $user The user to check the favourite against
*
* @return bool Whether or not the user has favourited the discussion
*/
public function is_favourited(discussion_entity $discussion, \context_module $forumcontext, \stdClass $user) {
if (!isloggedin()) {
return false;
}
$usercontext = \context_user::instance($user->id);
$ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
return $ufservice->favourite_exists('mod_forum', 'discussions', $discussion->get_id(), $forumcontext);
}
}
@@ -0,0 +1,299 @@
<?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/>.
/**
* Exported discussion summaries builder class.
*
* @package mod_forum
* @copyright 2019 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\builders;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\factories\manager as manager_factory;
use rating_manager;
use renderer_base;
use stdClass;
/**
* Exported discussion summaries builder class.
*
* This class is an implementation of the builder pattern (loosely). It is responsible
* for taking a set of related forums, discussions, and posts and generate the exported
* version of the discussion summaries.
*
* It encapsulates the complexity involved with exporting discussions summaries. All of the relevant
* additional resources will be loaded by this class in order to ensure the exporting
* process can happen.
*
* See this doc for more information on the builder pattern:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/Builder/README.html
*
* @package mod_forum
* @copyright 2019 Mihail Geshoski <mihail@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class exported_discussion_summaries {
/** @var renderer_base $renderer Core renderer */
private $renderer;
/** @var legacy_data_mapper_factory $legacydatamapperfactory Data mapper factory */
private $legacydatamapperfactory;
/** @var exporter_factory $exporterfactory Exporter factory */
private $exporterfactory;
/** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory;
/** @var manager_factory $managerfactory Manager factory */
private $managerfactory;
/** @var rating_manager $ratingmanager Rating manager */
private $ratingmanager;
/**
* Constructor.
*
* @param renderer_base $renderer Core renderer
* @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
* @param exporter_factory $exporterfactory Exporter factory
* @param vault_factory $vaultfactory Vault factory
* @param manager_factory $managerfactory Manager factory
*/
public function __construct(
renderer_base $renderer,
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
manager_factory $managerfactory
) {
$this->renderer = $renderer;
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->managerfactory = $managerfactory;
$this->ratingmanager = $managerfactory->get_rating_manager();
}
/**
* Build the exported discussion summaries for a given set of discussions.
*
* This will typically be used for a list of discussions in the same forum.
*
* @param stdClass $user The user to export the posts for.
* @param forum_entity $forum The forum that each of the $discussions belong to
* @param discussion_summary_entity[] $discussions A list of all discussion summaries to export
* @return stdClass[] List of exported posts in the same order as the $posts array.
*/
public function build(
stdClass $user,
forum_entity $forum,
array $discussions
): array {
$capabilitymanager = $this->managerfactory->get_capability_manager($forum);
$canseeanyprivatereply = $capabilitymanager->can_view_any_private_reply($user);
$discussionids = array_keys($discussions);
$postvault = $this->vaultfactory->get_post_vault();
$posts = $postvault->get_from_discussion_ids($user, $discussionids, $canseeanyprivatereply);
$groupsbyid = $this->get_groups_available_in_forum($forum);
$groupsbyauthorid = $this->get_author_groups_from_posts($posts, $forum);
$replycounts = $postvault->get_reply_count_for_discussion_ids($user, $discussionids, $canseeanyprivatereply);
$latestposts = $postvault->get_latest_posts_for_discussion_ids($user, $discussionids, $canseeanyprivatereply);
$latestauthors = $this->get_latest_posts_authors($latestposts);
$latestpostsids = array_map(function($post) {
return $post->get_id();
}, $latestposts);
$postauthorids = array_unique(array_reduce($discussions, function($carry, $summary) use ($latestposts){
$firstpostauthorid = $summary->get_first_post_author()->get_id();
$discussion = $summary->get_discussion();
$lastpostauthorid = $latestposts[$discussion->get_id()]->get_author_id();
return array_merge($carry, [$firstpostauthorid, $lastpostauthorid]);
}, []));
$postauthorcontextids = $this->get_author_context_ids($postauthorids);
$unreadcounts = [];
$favourites = $this->get_favourites($user);
$forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper();
$forumrecord = $forumdatamapper->to_legacy_object($forum);
if (forum_tp_can_track_forums($forumrecord)) {
$unreadcounts = $postvault->get_unread_count_for_discussion_ids($user, $discussionids, $canseeanyprivatereply);
}
$summaryexporter = $this->exporterfactory->get_discussion_summaries_exporter(
$user,
$forum,
$discussions,
$groupsbyid,
$groupsbyauthorid,
$replycounts,
$unreadcounts,
$latestpostsids,
$postauthorcontextids,
$favourites,
$latestauthors
);
$exportedposts = (array) $summaryexporter->export($this->renderer);
$firstposts = $postvault->get_first_post_for_discussion_ids($discussionids);
array_walk($exportedposts['summaries'], function($summary) use ($firstposts, $latestposts) {
$summary->discussion->times['created'] = (int) $firstposts[$summary->discussion->firstpostid]->get_time_created();
$summary->discussion->times['modified'] = (int) $latestposts[$summary->discussion->id]->get_time_created();
});
// Pass the current, preferred sort order for the discussions list.
$discussionlistvault = $this->vaultfactory->get_discussions_in_forum_vault();
$sortorder = get_user_preferences('forum_discussionlistsortorder',
$discussionlistvault::SORTORDER_LASTPOST_DESC);
$sortoptions = array(
'islastpostdesc' => $sortorder == $discussionlistvault::SORTORDER_LASTPOST_DESC,
'islastpostasc' => $sortorder == $discussionlistvault::SORTORDER_LASTPOST_ASC,
'isrepliesdesc' => $sortorder == $discussionlistvault::SORTORDER_REPLIES_DESC,
'isrepliesasc' => $sortorder == $discussionlistvault::SORTORDER_REPLIES_ASC,
'iscreateddesc' => $sortorder == $discussionlistvault::SORTORDER_CREATED_DESC,
'iscreatedasc' => $sortorder == $discussionlistvault::SORTORDER_CREATED_ASC,
'isdiscussiondesc' => $sortorder == $discussionlistvault::SORTORDER_DISCUSSION_DESC,
'isdiscussionasc' => $sortorder == $discussionlistvault::SORTORDER_DISCUSSION_ASC,
'isstarterdesc' => $sortorder == $discussionlistvault::SORTORDER_STARTER_DESC,
'isstarterasc' => $sortorder == $discussionlistvault::SORTORDER_STARTER_ASC,
'isgroupdesc' => $sortorder == $discussionlistvault::SORTORDER_GROUP_DESC,
'isgroupasc' => $sortorder == $discussionlistvault::SORTORDER_GROUP_ASC,
);
$exportedposts['state']['sortorder'] = $sortoptions;
return $exportedposts;
}
/**
* Get a list of all favourited discussions.
*
* @param stdClass $user The user we are getting favourites for
* @return int[] A list of favourited itemids
*/
private function get_favourites(stdClass $user): array {
$ids = [];
if (isloggedin()) {
$usercontext = \context_user::instance($user->id);
$ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
$favourites = $ufservice->find_favourites_by_type('mod_forum', 'discussions');
foreach ($favourites as $favourite) {
$ids[] = $favourite->itemid;
}
}
return $ids;
}
/**
* Returns a mapped array of discussionid to the authors of the latest post
*
* @param array $latestposts Mapped array of discussion to latest posts.
* @return array Array of authors mapped to the discussion
*/
private function get_latest_posts_authors($latestposts) {
$authors = $this->vaultfactory->get_author_vault()->get_authors_for_posts($latestposts);
$mappedauthors = array_reduce($latestposts, function($carry, $item) use ($authors) {
$carry[$item->get_discussion_id()] = $authors[$item->get_author_id()];
return $carry;
}, []);
return $mappedauthors;
}
/**
* Get the groups details for all groups available to the forum.
* @param forum_entity $forum The forum entity
* @return stdClass[]
*/
private function get_groups_available_in_forum($forum): array {
$course = $forum->get_course_record();
$coursemodule = $forum->get_course_module_record();
return groups_get_all_groups($course->id, 0, $coursemodule->groupingid);
}
/**
* Get the author's groups for a list of posts.
*
* @param post_entity[] $posts The list of posts
* @param forum_entity $forum The forum entity
* @return array Author groups indexed by author id
*/
private function get_author_groups_from_posts(array $posts, $forum): array {
$course = $forum->get_course_record();
$coursemodule = $forum->get_course_module_record();
$authorids = array_reduce($posts, function($carry, $post) {
$carry[$post->get_author_id()] = true;
return $carry;
}, []);
$authorgroups = groups_get_all_groups($course->id, array_keys($authorids), $coursemodule->groupingid,
'g.*, gm.id, gm.groupid, gm.userid');
$authorgroups = array_reduce($authorgroups, function($carry, $group) {
// Clean up data returned from groups_get_all_groups.
$userid = $group->userid;
$groupid = $group->groupid;
unset($group->userid);
unset($group->groupid);
$group->id = $groupid;
if (!isset($carry[$userid])) {
$carry[$userid] = [$group];
} else {
$carry[$userid][] = $group;
}
return $carry;
}, []);
foreach (array_diff(array_keys($authorids), array_keys($authorgroups)) as $authorid) {
$authorgroups[$authorid] = [];
}
return $authorgroups;
}
/**
* Get the user context ids for each of the authors.
*
* @param int[] $authorids The list of author ids to fetch context ids for.
* @return int[] Context ids indexed by author id
*/
private function get_author_context_ids(array $authorids): array {
$authorvault = $this->vaultfactory->get_author_vault();
return $authorvault->get_context_ids_for_author_ids($authorids);
}
}
@@ -0,0 +1,573 @@
<?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/>.
/**
* Exported post builder class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\builders;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\factories\manager as manager_factory;
use core_tag_tag;
use moodle_exception;
use renderer_base;
use stdClass;
/**
* Exported post builder class.
*
* This class is an implementation of the builder pattern (loosely). It is responsible
* for taking a set of related forums, discussions, and posts and generate the exported
* version of the posts.
*
* It encapsulates the complexity involved with exporting posts. All of the relevant
* additional resources will be loaded by this class in order to ensure the exporting
* process can happen.
*
* See this doc for more information on the builder pattern:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/Builder/README.html
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class exported_posts {
/** @var renderer_base $renderer Core renderer */
private $renderer;
/** @var legacy_data_mapper_factory $legacydatamapperfactory Data mapper factory */
private $legacydatamapperfactory;
/** @var exporter_factory $exporterfactory Exporter factory */
private $exporterfactory;
/** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory;
/** @var rating_manager $ratingmanager Rating manager */
private $ratingmanager;
/** @var manager_factory $managerfactory Manager factory */
private $managerfactory;
/**
* Constructor.
*
* @param renderer_base $renderer Core renderer
* @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
* @param exporter_factory $exporterfactory Exporter factory
* @param vault_factory $vaultfactory Vault factory
* @param manager_factory $managerfactory Manager factory
*/
public function __construct(
renderer_base $renderer,
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
manager_factory $managerfactory
) {
$this->renderer = $renderer;
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->managerfactory = $managerfactory;
$this->ratingmanager = $managerfactory->get_rating_manager();
}
/**
* Build the exported posts for a given set of forums, discussions, and posts.
*
* This will typically be used for a list of posts in the same discussion/forum however
* it does support exporting any arbitrary list of posts as long as the caller also provides
* a unique list of all discussions for the list of posts and all forums for the list of discussions.
*
* Increasing the number of different forums being processed will increase the processing time
* due to processing multiple contexts (for things like capabilities, files, etc). The code attempts
* to load the additional resources as efficiently as possible but there is no way around some of
* the additional overhead.
*
* Note: Some posts will be removed as part of the build process according to capabilities.
* A one-to-one mapping should not be expected.
*
* @param stdClass $user The user to export the posts for.
* @param forum_entity[] $forums A list of all forums that each of the $discussions belong to
* @param discussion_entity[] $discussions A list of all discussions that each of the $posts belong to
* @param post_entity[] $posts The list of posts to export.
* @param bool $includeinlineattachments Whether inline attachments should be included or not.
* @return stdClass[] List of exported posts in the same order as the $posts array.
*/
public function build(
stdClass $user,
array $forums,
array $discussions,
array $posts,
bool $includeinlineattachments = false
): array {
// Format the forums and discussion to make them more easily accessed later.
$forums = array_reduce($forums, function($carry, $forum) {
$carry[$forum->get_id()] = $forum;
return $carry;
}, []);
$discussions = array_reduce($discussions, function($carry, $discussion) {
$carry[$discussion->get_id()] = $discussion;
return $carry;
}, []);
// Group the posts by discussion and forum so that we can load the resources in
// batches to improve performance.
$groupedposts = $this->group_posts_by_discussion($forums, $discussions, $posts);
// Load all of the resources we need in order to export the posts.
$authorsbyid = $this->get_authors_for_posts($posts);
$authorcontextids = $this->get_author_context_ids(array_keys($authorsbyid));
$attachmentsbypostid = $this->get_attachments_for_posts($groupedposts);
$inlineattachments = [];
if ($includeinlineattachments) {
$inlineattachments = $this->get_inline_attachments_for_posts($groupedposts);
}
$groupsbycourseandauthorid = $this->get_author_groups_from_posts($groupedposts);
$tagsbypostid = $this->get_tags_from_posts($posts);
$ratingbypostid = $this->get_ratings_from_posts($user, $groupedposts);
$readreceiptcollectionbyforumid = $this->get_read_receipts_from_posts($user, $groupedposts);
$exportedposts = [];
// Export each set of posts per discussion because it's the largest chunks we can
// break them into due to constraints on capability checks.
foreach ($groupedposts as $grouping) {
[
'forum' => $forum,
'discussion' => $discussion,
'posts' => $groupedposts
] = $grouping;
// Exclude posts the user cannot see, such as certain posts in Q and A forums.
$capabilitymanager = $this->managerfactory->get_capability_manager($forum);
$groupedposts = array_filter($groupedposts, function($post) use ($capabilitymanager, $user, $discussion) {
return $capabilitymanager->can_view_post($user, $discussion, $post);
});
$forumid = $forum->get_id();
$courseid = $forum->get_course_record()->id;
$postsexporter = $this->exporterfactory->get_posts_exporter(
$user,
$forum,
$discussion,
$groupedposts,
$authorsbyid,
$authorcontextids,
$attachmentsbypostid,
$groupsbycourseandauthorid[$courseid],
$readreceiptcollectionbyforumid[$forumid] ?? null,
$tagsbypostid,
$ratingbypostid,
true,
$inlineattachments
);
['posts' => $exportedgroupedposts] = (array) $postsexporter->export($this->renderer);
$exportedposts = array_merge($exportedposts, $exportedgroupedposts);
}
if (count($forums) == 1 && count($discussions) == 1) {
// All of the posts belong to a single discussion in a single forum so
// the exported order will match the given $posts array.
return $exportedposts;
} else {
// Since we grouped the posts by discussion and forum the ordering of the
// exported posts may be different to the given $posts array so we should
// sort it back into the correct order for the caller.
return $this->sort_exported_posts($posts, $exportedposts);
}
}
/**
* Group the posts by which discussion they belong to in order for them to be processed
* in chunks by the exporting.
*
* Returns a list of groups where each group has a forum, discussion, and list of posts.
* E.g.
* [
* [
* 'forum' => <forum_entity>,
* 'discussion' => <discussion_entity>,
* 'posts' => [
* <post_entity in discussion>,
* <post_entity in discussion>,
* <post_entity in discussion>
* ]
* ]
* ]
*
* @param forum_entity[] $forums A list of all forums that each of the $discussions belong to, indexed by id.
* @param discussion_entity[] $discussions A list of all discussions that each of the $posts belong to, indexed by id.
* @param post_entity[] $posts The list of posts to process.
* @return array List of grouped posts. Each group has a discussion, forum, and posts.
*/
private function group_posts_by_discussion(array $forums, array $discussions, array $posts): array {
return array_reduce($posts, function($carry, $post) use ($forums, $discussions) {
$discussionid = $post->get_discussion_id();
if (!isset($discussions[$discussionid])) {
throw new moodle_exception('Unable to find discussion with id ' . $discussionid);
}
if (isset($carry[$discussionid])) {
$carry[$discussionid]['posts'][] = $post;
} else {
$discussion = $discussions[$discussionid];
$forumid = $discussion->get_forum_id();
if (!isset($forums[$forumid])) {
throw new moodle_exception('Unable to find forum with id ' . $forumid);
}
$carry[$discussionid] = [
'forum' => $forums[$forumid],
'discussion' => $discussions[$discussionid],
'posts' => [$post]
];
}
return $carry;
}, []);
}
/**
* Load the list of authors for the given posts.
*
* The list of authors will be indexed by the author id.
*
* @param post_entity[] $posts The list of posts to process.
* @return author_entity[]
*/
private function get_authors_for_posts(array $posts): array {
$authorvault = $this->vaultfactory->get_author_vault();
return $authorvault->get_authors_for_posts($posts);
}
/**
* Get the user context ids for each of the authors.
*
* @param int[] $authorids The list of author ids to fetch context ids for.
* @return int[] Context ids indexed by author id
*/
private function get_author_context_ids(array $authorids): array {
$authorvault = $this->vaultfactory->get_author_vault();
return $authorvault->get_context_ids_for_author_ids($authorids);
}
/**
* Load the list of all inline attachments for the posts. The list of attachments will be
* indexed by the post id.
*
* @param array $groupedposts List of posts grouped by discussions.
* @return stored_file[]
*/
private function get_inline_attachments_for_posts(array $groupedposts): array {
$inlineattachmentsbypostid = [];
$postattachmentvault = $this->vaultfactory->get_post_attachment_vault();
$postsbyforum = array_reduce($groupedposts, function($carry, $grouping) {
['forum' => $forum, 'posts' => $posts] = $grouping;
$forumid = $forum->get_id();
if (!isset($carry[$forumid])) {
$carry[$forumid] = [
'forum' => $forum,
'posts' => []
];
}
$carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts);
return $carry;
}, []);
foreach ($postsbyforum as $grouping) {
['forum' => $forum, 'posts' => $posts] = $grouping;
$inlineattachments = $postattachmentvault->get_inline_attachments_for_posts($forum->get_context(), $posts);
// Have to loop in order to maintain the correct indexes since they are numeric.
foreach ($inlineattachments as $postid => $attachment) {
$inlineattachmentsbypostid[$postid] = $attachment;
}
}
return $inlineattachmentsbypostid;
}
/**
* Load the list of all attachments for the posts. The list of attachments will be
* indexed by the post id.
*
* @param array $groupedposts List of posts grouped by discussions.
* @return stored_file[]
*/
private function get_attachments_for_posts(array $groupedposts): array {
$attachmentsbypostid = [];
$postattachmentvault = $this->vaultfactory->get_post_attachment_vault();
$postsbyforum = array_reduce($groupedposts, function($carry, $grouping) {
['forum' => $forum, 'posts' => $posts] = $grouping;
$forumid = $forum->get_id();
if (!isset($carry[$forumid])) {
$carry[$forumid] = [
'forum' => $forum,
'posts' => []
];
}
$carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts);
return $carry;
}, []);
foreach ($postsbyforum as $grouping) {
['forum' => $forum, 'posts' => $posts] = $grouping;
$attachments = $postattachmentvault->get_attachments_for_posts($forum->get_context(), $posts);
// Have to loop in order to maintain the correct indexes since they are numeric.
foreach ($attachments as $postid => $attachment) {
$attachmentsbypostid[$postid] = $attachment;
}
}
return $attachmentsbypostid;
}
/**
* Get the groups for each author of the given posts.
*
* The results are grouped by course and then author id because the groups are
* contextually related to the course, e.g. a single author can be part of two different
* sets of groups in two different courses.
*
* @param array $groupedposts List of posts grouped by discussions.
* @return array List of groups indexed by forum id and then author id.
*/
private function get_author_groups_from_posts(array $groupedposts): array {
$groupsbyauthorid = [];
$authoridsbycourseid = [];
// Get the unique list of author ids for each course in the grouped
// posts. Grouping by course is the largest grouping we can achieve.
foreach ($groupedposts as $grouping) {
['forum' => $forum, 'posts' => $posts] = $grouping;
$course = $forum->get_course_record();
$courseid = $course->id;
if (!isset($authoridsbycourseid[$courseid])) {
$coursemodule = $forum->get_course_module_record();
$authoridsbycourseid[$courseid] = [
'groupingid' => $coursemodule->groupingid,
'authorids' => []
];
}
$authorids = array_map(function($post) {
return $post->get_author_id();
}, $posts);
foreach ($authorids as $authorid) {
$authoridsbycourseid[$courseid]['authorids'][$authorid] = $authorid;
}
}
// Load each set of groups per course.
foreach ($authoridsbycourseid as $courseid => $values) {
['groupingid' => $groupingid, 'authorids' => $authorids] = $values;
$authorgroups = groups_get_all_groups(
$courseid,
array_keys($authorids),
$groupingid,
'g.*, gm.id, gm.groupid, gm.userid'
);
if (!isset($groupsbyauthorid[$courseid])) {
$groupsbyauthorid[$courseid] = [];
}
foreach ($authorgroups as $group) {
// Clean up data returned from groups_get_all_groups.
$userid = $group->userid;
$groupid = $group->groupid;
unset($group->userid);
unset($group->groupid);
$group->id = $groupid;
if (!isset($groupsbyauthorid[$courseid][$userid])) {
$groupsbyauthorid[$courseid][$userid] = [];
}
$groupsbyauthorid[$courseid][$userid][] = $group;
}
}
return $groupsbyauthorid;
}
/**
* Get the list of tags for each of the posts. The tags will be returned in an
* array indexed by the post id.
*
* @param post_entity[] $posts The list of posts to load tags for.
* @return array Sets of tags indexed by post id.
*/
private function get_tags_from_posts(array $posts): array {
$postids = array_map(function($post) {
return $post->get_id();
}, $posts);
return core_tag_tag::get_items_tags('mod_forum', 'forum_posts', $postids);
}
/**
* Get the list of ratings for each post. The ratings are returned in an array
* indexed by the post id.
*
* @param stdClass $user The user viewing the ratings.
* @param array $groupedposts List of posts grouped by discussions.
* @return array Sets of ratings indexed by post id.
*/
private function get_ratings_from_posts(stdClass $user, array $groupedposts) {
$ratingsbypostid = [];
$postsdatamapper = $this->legacydatamapperfactory->get_post_data_mapper();
$postsbyforum = array_reduce($groupedposts, function($carry, $grouping) {
['forum' => $forum, 'posts' => $posts] = $grouping;
$forumid = $forum->get_id();
if (!isset($carry[$forumid])) {
$carry[$forumid] = [
'forum' => $forum,
'posts' => []
];
}
$carry[$forumid]['posts'] = array_merge($carry[$forumid]['posts'], $posts);
return $carry;
}, []);
foreach ($postsbyforum as $grouping) {
['forum' => $forum, 'posts' => $posts] = $grouping;
if (!$forum->has_rating_aggregate()) {
continue;
}
$items = $postsdatamapper->to_legacy_objects($posts);
$ratingoptions = (object) [
'context' => $forum->get_context(),
'component' => 'mod_forum',
'ratingarea' => 'post',
'items' => $items,
'aggregate' => $forum->get_rating_aggregate(),
'scaleid' => $forum->get_scale(),
'userid' => $user->id,
'assesstimestart' => $forum->get_assess_time_start(),
'assesstimefinish' => $forum->get_assess_time_finish()
];
$rm = $this->ratingmanager;
$items = $rm->get_ratings($ratingoptions);
foreach ($items as $item) {
$ratingsbypostid[$item->id] = empty($item->rating) ? null : $item->rating;
}
}
return $ratingsbypostid;
}
/**
* Get the read receipt collections for the given viewing user and each forum. The
* receipt collections will only be loaded for posts in forums that the user is tracking.
*
* The receipt collections are returned in an array indexed by the forum ids.
*
* @param stdClass $user The user viewing the posts.
* @param array $groupedposts List of posts grouped by discussions.
*/
private function get_read_receipts_from_posts(stdClass $user, array $groupedposts) {
$forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper();
$trackedforums = [];
$trackedpostids = [];
foreach ($groupedposts as $group) {
['forum' => $forum, 'posts' => $posts] = $group;
$forumid = $forum->get_id();
if (!isset($trackedforums[$forumid])) {
$forumrecord = $forumdatamapper->to_legacy_object($forum);
$trackedforums[$forumid] = forum_tp_is_tracked($forumrecord, $user);
}
if ($trackedforums[$forumid]) {
foreach ($posts as $post) {
$trackedpostids[] = $post->get_id();
}
}
}
if (empty($trackedpostids)) {
return [];
}
// We can just load a single receipt collection for all tracked posts.
$receiptvault = $this->vaultfactory->get_post_read_receipt_collection_vault();
$readreceiptcollection = $receiptvault->get_from_user_id_and_post_ids($user->id, $trackedpostids);
$receiptsbyforumid = [];
// Assign the collection to all forums that are tracked.
foreach ($trackedforums as $forumid => $tracked) {
if ($tracked) {
$receiptsbyforumid[$forumid] = $readreceiptcollection;
}
}
return $receiptsbyforumid;
}
/**
* Sort the list of exported posts back into the same order as the given posts.
* The ordering of the exported posts can often deviate from the given posts due
* to the process of exporting them so we need to sort them back into the order
* that the calling code expected.
*
* @param post_entity[] $posts The posts in the expected order.
* @param stdClass[] $exportedposts The list of exported posts in any order.
* @return stdClass[] Sorted exported posts.
*/
private function sort_exported_posts(array $posts, array $exportedposts) {
$postindexes = [];
foreach (array_values($posts) as $index => $post) {
$postindexes[$post->get_id()] = $index;
}
$sortedexportedposts = [];
foreach ($exportedposts as $exportedpost) {
$index = $postindexes[$exportedpost->id];
$sortedexportedposts[$index] = $exportedpost;
}
return $sortedexportedposts;
}
}
+154
View File
@@ -0,0 +1,154 @@
<?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/>.
/**
* Container class.
*
* @package mod_forum
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\factories\renderer as renderer_factory;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\entity as entity_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\manager as manager_factory;
use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\factories\builder as builder_factory;
use mod_forum\local\factories\url as url_factory;
/**
* Container class.
*
* This class provides helper methods with static configurations to get any
* of the factories from the "local" namespace.
*
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class container {
/**
* Create the renderer factory.
*
* @return renderer_factory
*/
public static function get_renderer_factory(): renderer_factory {
global $PAGE;
return new renderer_factory(
self::get_legacy_data_mapper_factory(),
self::get_exporter_factory(),
self::get_vault_factory(),
self::get_manager_factory(),
self::get_entity_factory(),
self::get_builder_factory(),
self::get_url_factory(),
$PAGE
);
}
/**
* Create the legacy data mapper factory.
*
* @return legacy_data_mapper_factory
*/
public static function get_legacy_data_mapper_factory(): legacy_data_mapper_factory {
return new legacy_data_mapper_factory();
}
/**
* Create the exporter factory.
*
* @return exporter_factory
*/
public static function get_exporter_factory(): exporter_factory {
return new exporter_factory(
self::get_legacy_data_mapper_factory(),
self::get_manager_factory(),
self::get_url_factory(),
self::get_vault_factory()
);
}
/**
* Create the vault factory.
*
* @return vault_factory
*/
public static function get_vault_factory(): vault_factory {
global $DB;
return new vault_factory(
$DB,
self::get_entity_factory(),
get_file_storage(),
self::get_legacy_data_mapper_factory()
);
}
/**
* Create the manager factory.
*
* @return manager_factory
*/
public static function get_manager_factory(): manager_factory {
return new manager_factory(
self::get_legacy_data_mapper_factory()
);
}
/**
* Create the entity factory.
*
* @return entity_factory
*/
public static function get_entity_factory(): entity_factory {
return new entity_factory();
}
/**
* Create the builder factory.
*
* @return builder_factory
*/
public static function get_builder_factory(): builder_factory {
global $PAGE;
return new builder_factory(
self::get_legacy_data_mapper_factory(),
self::get_exporter_factory(),
self::get_vault_factory(),
self::get_manager_factory(),
$PAGE->get_renderer('mod_forum')
);
}
/**
* Create the URL factory.
*
* @return url_factory
*/
public static function get_url_factory(): url_factory {
return new url_factory(
self::get_legacy_data_mapper_factory()
);
}
}
@@ -0,0 +1,73 @@
<?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/>.
/**
* Author data mapper.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\data_mappers\legacy;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\author as author_entity;
use stdClass;
/**
* Convert an author entity into an stdClass.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class author {
/**
* Convert a list of author entities into stdClasses.
*
* @param author_entity[] $authors The authors to convert.
* @return stdClass[]
*/
public function to_legacy_objects(array $authors): array {
return array_map(function(author_entity $author) {
return (object) [
'id' => $author->get_id(),
'picture' => $author->get_picture_item_id(),
'firstname' => $author->get_first_name(),
'lastname' => $author->get_last_name(),
'fullname' => $author->get_full_name(),
'email' => $author->get_email(),
'deleted' => $author->is_deleted(),
'middlename' => $author->get_middle_name(),
'firstnamephonetic' => $author->get_first_name_phonetic(),
'lastnamephonetic' => $author->get_last_name_phonetic(),
'alternatename' => $author->get_alternate_name(),
'imagealt' => $author->get_image_alt()
];
}, $authors);
}
/**
* Convert an author entity into an stdClass.
*
* @param author_entity $author The author to convert.
* @return stdClass
*/
public function to_legacy_object(author_entity $author): stdClass {
return $this->to_legacy_objects([$author])[0];
}
}
@@ -0,0 +1,75 @@
<?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/>.
/**
* Discussion data mapper.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\data_mappers\legacy;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use stdClass;
/**
* Convert a discussion entity into an stdClass.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion {
/**
* Convert a list of discussion entities into stdClasses.
*
* @param discussion_entity[] $authors The authors to convert.
* @return stdClass[]
*/
public function to_legacy_objects(array $discussions): array {
return array_map(function(discussion_entity $discussion) {
return (object) [
'id' => $discussion->get_id(),
'course' => $discussion->get_course_id(),
'forum' => $discussion->get_forum_id(),
'name' => $discussion->get_name(),
'firstpost' => $discussion->get_first_post_id(),
'userid' => $discussion->get_user_id(),
'groupid' => $discussion->get_group_id(),
'assessed' => $discussion->is_assessed(),
'timemodified' => $discussion->get_time_modified(),
'usermodified' => $discussion->get_user_modified(),
'timestart' => $discussion->get_time_start(),
'timeend' => $discussion->get_time_end(),
'pinned' => $discussion->is_pinned(),
'timelocked' => $discussion->get_locked()
];
}, $discussions);
}
/**
* Convert a discussion entity into an stdClass.
*
* @param discussion_entity $discussion The discussion to convert.
* @return stdClass
*/
public function to_legacy_object(discussion_entity $discussion): stdClass {
return $this->to_legacy_objects([$discussion])[0];
}
}
@@ -0,0 +1,90 @@
<?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/>.
/**
* Forum data mapper.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\data_mappers\legacy;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\forum as forum_entity;
use stdClass;
/**
* Convert a forum entity into an stdClass.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class forum {
/**
* Convert a list of forum entities into stdClasses.
*
* @param forum_entity[] $forums The forums to convert.
* @return stdClass[]
*/
public function to_legacy_objects(array $forums): array {
return array_map(function(forum_entity $forum) {
return (object) [
'id' => $forum->get_id(),
'course' => $forum->get_course_id(),
'type' => $forum->get_type(),
'name' => $forum->get_name(),
'intro' => $forum->get_intro(),
'introformat' => $forum->get_intro_format(),
'assessed' => $forum->get_rating_aggregate(),
'assesstimestart' => $forum->get_assess_time_start(),
'assesstimefinish' => $forum->get_assess_time_finish(),
'scale' => $forum->get_scale(),
'grade_forum' => $forum->get_grade_for_forum(),
'grade_forum_notify' => $forum->should_notify_students_default_when_grade_for_forum(),
'maxbytes' => $forum->get_max_bytes(),
'maxattachments' => $forum->get_max_attachments(),
'forcesubscribe' => $forum->get_subscription_mode(),
'trackingtype' => $forum->get_tracking_type(),
'rsstype' => $forum->get_rss_type(),
'rssarticles' => $forum->get_rss_articles(),
'timemodified' => $forum->get_time_modified(),
'warnafter' => $forum->get_warn_after(),
'blockafter' => $forum->get_block_after(),
'blockperiod' => $forum->get_block_period(),
'completiondiscussions' => $forum->get_completion_discussions(),
'completionreplies' => $forum->get_completion_replies(),
'completionposts' => $forum->get_completion_posts(),
'displaywordcount' => $forum->should_display_word_count(),
'lockdiscussionafter' => $forum->get_lock_discussions_after(),
'duedate' => $forum->get_due_date(),
'cutoffdate' => $forum->get_cutoff_date()
];
}, $forums);
}
/**
* Convert a forum entity into an stdClass.
*
* @param forum_entity $forum The forum to convert.
* @return stdClass
*/
public function to_legacy_object(forum_entity $forum): stdClass {
return $this->to_legacy_objects([$forum])[0];
}
}
@@ -0,0 +1,79 @@
<?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/>.
/**
* Post data mapper.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\data_mappers\legacy;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\post as post_entity;
use stdClass;
/**
* Convert a post entity into an stdClass.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post {
/**
* Convert a list of post entities into stdClasses.
*
* @param post_entity[] $posts The posts to convert.
* @return stdClass[]
*/
public function to_legacy_objects(array $posts): array {
return array_map(function(post_entity $post) {
return (object) [
'id' => $post->get_id(),
'discussion' => $post->get_discussion_id(),
'parent' => $post->get_parent_id(),
'userid' => $post->get_author_id(),
'created' => $post->get_time_created(),
'modified' => $post->get_time_modified(),
'mailed' => $post->has_been_mailed(),
'subject' => $post->get_subject(),
'message' => $post->get_message(),
'messageformat' => $post->get_message_format(),
'messagetrust' => $post->is_message_trusted(),
'attachment' => $post->has_attachments(),
'totalscore' => $post->get_total_score(),
'mailnow' => $post->should_mail_now(),
'deleted' => $post->is_deleted(),
'privatereplyto' => $post->get_private_reply_recipient_id(),
'wordcount' => $post->get_wordcount(),
'charcount' => $post->get_charcount(),
];
}, $posts);
}
/**
* Convert a post entity into an stdClass.
*
* @param post_entity $post The post to convert.
* @return stdClass
*/
public function to_legacy_object(post_entity $post): stdClass {
return $this->to_legacy_objects([$post])[0];
}
}
+211
View File
@@ -0,0 +1,211 @@
<?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/>.
/**
* Author class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\entities;
defined('MOODLE_INTERNAL') || die();
/**
* Author class.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class author {
/** @var int $id ID */
private $id;
/** @var int $pictureitemid Picture item id */
private $pictureitemid;
/** @var string $firstname First name */
private $firstname;
/** @var string $lastname Last name */
private $lastname;
/** @var string $fullname Full name */
private $fullname;
/** @var string $email Email */
private $email;
/** @var bool $deleted Deleted */
private $deleted;
/** @var string $middlename Middle name */
private $middlename;
/** @var string $firstnamephonetic Phonetic spelling of first name */
private $firstnamephonetic;
/** @var string $lastnamephonetic Phonetic spelling of last name */
private $lastnamephonetic;
/** @var string $alternatename Altername name */
private $alternatename;
/** @var string $imagealt Image alt */
private $imagealt;
/**
* Constructor.
*
* @param int $id ID
* @param int $pictureitemid Picture item id
* @param string $firstname First name
* @param string $lastname Last name
* @param string $fullname Full name
* @param string $email Email
* @param string|null $middlename Middle name
* @param string|null $firstnamephonetic Phonetic spelling of first name
* @param string|null $lastnamephonetic Phonetic spelling of last name
* @param string|null $alternatename Altername name
* @param string|null $imagealt Image alt
*/
public function __construct(
int $id,
int $pictureitemid,
string $firstname,
string $lastname,
string $fullname,
string $email,
bool $deleted,
string $middlename = null,
string $firstnamephonetic = null,
string $lastnamephonetic = null,
string $alternatename = null,
string $imagealt = null
) {
$this->id = $id;
$this->pictureitemid = $pictureitemid;
$this->firstname = $firstname;
$this->lastname = $lastname;
$this->fullname = $fullname;
$this->email = $email;
$this->deleted = $deleted;
$this->middlename = $middlename;
$this->firstnamephonetic = $firstnamephonetic;
$this->lastnamephonetic = $lastnamephonetic;
$this->alternatename = $alternatename;
$this->imagealt = $imagealt;
}
/**
* Return the id.
*
* @return int
*/
public function get_id(): int {
return $this->id;
}
/**
* Return the picture item id.
*
* @return int
*/
public function get_picture_item_id(): int {
return $this->pictureitemid;
}
/**
* Return the first name.
*
* @return string
*/
public function get_first_name(): string {
return $this->firstname;
}
/**
* Return the last name.
*
* @return string
*/
public function get_last_name(): string {
return $this->lastname;
}
/**
* Return the full name.
*
* @return string
*/
public function get_full_name(): string {
return $this->fullname;
}
/**
* Return the email.
*
* @return string
*/
public function get_email(): string {
return $this->email;
}
/**
* Is the author deleted?
*
* @return bool
*/
public function is_deleted(): bool {
return !empty($this->deleted);
}
/**
* Return the middle name.
*
* @return string|null
*/
public function get_middle_name(): ?string {
return $this->middlename;
}
/**
* Return the first name phonetic.
*
* @return string|null
*/
public function get_first_name_phonetic(): ?string {
return $this->firstnamephonetic;
}
/**
* Return the last name phonetic.
*
* @return string|null
*/
public function get_last_name_phonetic(): ?string {
return $this->lastnamephonetic;
}
/**
* Return the alternate name.
*
* @return string|null
*/
public function get_alternate_name(): ?string {
return $this->alternatename;
}
/**
* Return the image alt.
*
* @return string|null
*/
public function get_image_alt(): ?string {
return $this->imagealt;
}
}
@@ -0,0 +1,335 @@
<?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/>.
/**
* Discussion class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\entities;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\post as post_entity;
/**
* Discussion class.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion {
/** @var int $id ID */
private $id;
/** @var int $courseid Course id */
private $courseid;
/** @var int $forumid Forum id */
private $forumid;
/** @var string $name Discussion name */
private $name;
/** @var int $firstpostid Id of the first post in the discussion */
private $firstpostid;
/** @var int $userid Id of the user that created the discussion */
private $userid;
/** @var int $groupid Group id if it's a group dicussion */
private $groupid;
/** @var bool $assessed Is the discussion assessed? */
private $assessed;
/** @var int $timemodified Timestamp for last modification to the discussion */
private $timemodified;
/** @var int $usermodified Id of user that last modified the discussion */
private $usermodified;
/** @var int $timestart Start time for the discussion */
private $timestart;
/** @var int $timeend End time for the discussion */
private $timeend;
/** @var bool $pinned Is the discussion pinned? */
private $pinned;
/** @var int $locked The timestamp of when the discussion was locked */
private $timelocked;
/**
* Constructor.
*
* @param int $id ID
* @param int $courseid Course id
* @param int $forumid Forum id
* @param string $name Discussion name
* @param int $firstpostid Id of the first post in the discussion
* @param int $userid Id of the user that created the discussion
* @param int $groupid Group id if it's a group dicussion
* @param bool $assessed Is the discussion assessed?
* @param int $timemodified Timestamp for last modification to the discussion
* @param int $usermodified Id of user that last modified the discussion
* @param int $timestart Start time for the discussion
* @param int $timeend End time for the discussion
* @param bool $pinned Is the discussion pinned?
* @param int $locked Time this discussion was locked
*/
public function __construct(
int $id,
int $courseid,
int $forumid,
string $name,
int $firstpostid,
int $userid,
int $groupid,
bool $assessed,
int $timemodified,
int $usermodified,
int $timestart,
int $timeend,
bool $pinned,
int $locked
) {
$this->id = $id;
$this->courseid = $courseid;
$this->forumid = $forumid;
$this->name = $name;
$this->firstpostid = $firstpostid;
$this->userid = $userid;
$this->groupid = $groupid;
$this->assessed = $assessed;
$this->timemodified = $timemodified;
$this->usermodified = $usermodified;
$this->timestart = $timestart;
$this->timeend = $timeend;
$this->pinned = $pinned;
$this->timelocked = $locked;
}
/**
* Get the discussion id.
*
* @return int
*/
public function get_id(): int {
return $this->id;
}
/**
* Get the course id.
*
* @return int
*/
public function get_course_id(): int {
return $this->courseid;
}
/**
* Get the forum id.
*
* @return int
*/
public function get_forum_id(): int {
return $this->forumid;
}
/**
* Get the name of the discussion.
*
* @return string
*/
public function get_name(): string {
return $this->name;
}
/**
* Get the id of the fist post in the discussion.
*
* @return int
*/
public function get_first_post_id(): int {
return $this->firstpostid;
}
/**
* Get the id of the user that created the discussion.
*
* @return int
*/
public function get_user_id(): int {
return $this->userid;
}
/**
* Get the id of the group that this discussion belongs to.
*
* @return int
*/
public function get_group_id(): int {
return $this->groupid;
}
/**
* Check if this discussion is assessed.
*
* @return bool
*/
public function is_assessed(): bool {
return $this->assessed;
}
/**
* Get the timestamp for when this discussion was last modified.
*
* @return int
*/
public function get_time_modified(): int {
return $this->timemodified;
}
/**
* Get the id of the user that last modified this discussion.
*
* @return int
*/
public function get_user_modified(): int {
return $this->usermodified;
}
/**
* Get the start time of this discussion. Returns zero if the discussion
* has no designated start time.
*
* @return int
*/
public function get_time_start(): int {
return $this->timestart;
}
/**
* Get the end time of this discussion. Returns zero if the discussion
* has no designated end time.
*
* @return int
*/
public function get_time_end(): int {
return $this->timeend;
}
/**
* Check if this discussion is pinned.
*
* @return bool
*/
public function is_pinned(): bool {
return $this->pinned;
}
/**
* Get the locked time of this discussion.
*
* @return bool
*/
public function get_locked(): int {
return $this->timelocked;
}
/**
* Is this discussion locked based on it's locked attribute
*
* @return bool
*/
public function is_locked(): bool {
return ($this->timelocked ? true : false);
}
/**
* Set the locked timestamp
*
* @param int $timestamp The value we want to store into 'locked'
*/
public function toggle_locked_state(int $timestamp) {
// Check the current value against what we want the value to be i.e. '$timestamp'.
$this->timelocked = ($this->timelocked && $timestamp ? $this->timelocked : $timestamp);
}
/**
* Check if the given post is the first post in this discussion.
*
* @param post_entity $post The post to check
* @return bool
*/
public function is_first_post(post_entity $post): bool {
return $this->get_first_post_id() === $post->get_id();
}
/**
* Check if the discussion has started yet. DEFAULTS: true if not set
*
* @return bool
*/
public function has_started(): bool {
$startime = $this->get_time_start();
return empty($startime) || $startime < time();
}
/**
* Check if the discussion has ended. DEFAULTS: false if not set
*
* @return bool
*/
public function has_ended(): bool {
$endtime = $this->get_time_end();
return !empty($endtime) && $endtime < time();
}
/**
* Check if the discussion belongs to a group.
*
* @return bool
*/
public function has_group(): bool {
return $this->get_group_id() > 0;
}
/**
* Set the pinned value for this entity
*
* @param int $targetstate The state to change the pin to
* @return bool
*/
public function set_pinned(int $targetstate): void {
if ($targetstate != $this->pinned) {
$this->pinned = $targetstate;
}
}
/**
* Check if the discussion is timed.
*
* @return bool
*/
public function is_timed_discussion(): bool {
global $CFG;
return !empty($CFG->forum_enabletimedposts) &&
($this->get_time_start() || $this->get_time_end());
}
/**
* Check if the timed discussion is visible.
*
* @return bool
*/
public function is_timed_discussion_visible(): bool {
return !$this->is_timed_discussion() || ($this->has_started() && !$this->has_ended());
}
}
@@ -0,0 +1,104 @@
<?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/>.
/**
* Discussion summary class.
*
* @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\entities;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\entities\author as author_entity;
/**
* Discussion summary class.
*
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion_summary {
/** @var discussion_entity $discussion The discussion being summarised */
private $discussion;
/** @var author_entity $firstpostauthor Author of the first post in the discussion */
private $firstpostauthor;
/** @var post_entity $firstpost First post in the discussion */
private $firstpost;
/** @var author_entity $latestpostauthor Author of the last post in the discussion */
private $latestpostauthor;
/**
* Constructor.
*
* @param discussion_entity $discussion The discussion being summarised
* @param post_entity $firstpost First post in the discussion
* @param author_entity $firstpostauthor Author of the first post in the discussion
* @param author_entity $latestpostauthor Author of the last post in the discussion
*/
public function __construct(
discussion_entity $discussion,
post_entity $firstpost,
author_entity $firstpostauthor,
author_entity $latestpostauthor
) {
$this->discussion = $discussion;
$this->firstpostauthor = $firstpostauthor;
$this->firstpost = $firstpost;
$this->latestpostauthor = $latestpostauthor;
}
/**
* Get the discussion entity.
*
* @return discussion_entity
*/
public function get_discussion(): discussion_entity {
return $this->discussion;
}
/**
* Get the author entity for the first post.
*
* @return author_entity
*/
public function get_first_post_author(): author_entity {
return $this->firstpostauthor;
}
/**
* Get the author entity for the last post.
*
* @return author_entity
*/
public function get_latest_post_author(): author_entity {
return $this->latestpostauthor;
}
/**
* Get the post entity for the first post.
*
* @return post_entity
*/
public function get_first_post(): post_entity {
return $this->firstpost;
}
}
+672
View File
@@ -0,0 +1,672 @@
<?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/>.
/**
* Forum class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\entities;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/forum/lib.php');
require_once($CFG->dirroot . '/rating/lib.php');
use mod_forum\local\entities\discussion as discussion_entity;
use context;
use stdClass;
/**
* Forum class.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class forum {
/** @var context $context The forum module context */
private $context;
/** @var stdClass $coursemodule The forum course module record */
private $coursemodule;
/** @var stdClass $course The forum course record */
private $course;
/** @var int $effectivegroupmode The effective group mode */
private $effectivegroupmode;
/** @var int $id ID */
private $id;
/** @var int $courseid Id of the course this forum is in */
private $courseid;
/** @var string $type The forum type, e.g. single, qanda, etc */
private $type;
/** @var string $name Name of the forum */
private $name;
/** @var string $intro Intro text */
private $intro;
/** @var int $introformat Format of the intro text */
private $introformat;
/** @var int $assessed The forum rating aggregate */
private $assessed;
/** @var int $assesstimestart Timestamp to begin assessment */
private $assesstimestart;
/** @var int $assesstimefinish Timestamp to end assessment */
private $assesstimefinish;
/** @var int $scale The rating scale */
private $scale;
/** @var int $gradeforum The grade for the forum when grading holistically */
private $gradeforum;
/** @var bool $gradeforumnotify Whether to notify students when the forum is graded holistically */
private $gradeforumnotify;
/** @var int $maxbytes Maximum attachment size */
private $maxbytes;
/** @var int $maxattachments Maximum number of attachments */
private $maxattachments;
/** @var int $forcesubscribe Does the forum force users to subscribe? */
private $forcesubscribe;
/** @var int $trackingtype Tracking type */
private $trackingtype;
/** @var int $rsstype RSS type */
private $rsstype;
/** @var int $rssarticles RSS articles */
private $rssarticles;
/** @var int $timemodified Timestamp when the forum was last modified */
private $timemodified;
/** @var int $warnafter Warn after */
private $warnafter;
/** @var int $blockafter Block after */
private $blockafter;
/** @var int $blockperiod Block period */
private $blockperiod;
/** @var int $completiondiscussions Completion discussions */
private $completiondiscussions;
/** @var int $completionreplies Completion replies */
private $completionreplies;
/** @var int $completionposts Completion posts */
private $completionposts;
/** @var bool $displaywordcount Should display word counts in posts */
private $displaywordcount;
/** @var bool $lockdiscussionafter Timestamp after which discussions should be locked */
private $lockdiscussionafter;
/** @var int $duedate Timestamp that represents the due date for forum posts */
private $duedate;
/** @var int $cutoffdate Timestamp after which forum posts will no longer be accepted */
private $cutoffdate;
/**
* Constructor
*
* @param context $context The forum module context
* @param stdClass $coursemodule The forum course module record
* @param stdClass $course The forum course record
* @param int $effectivegroupmode The effective group mode
* @param int $id ID
* @param int $courseid Id of the course this forum is in
* @param string $type The forum type, e.g. single, qanda, etc
* @param string $name Name of the forum
* @param string $intro Intro text
* @param int $introformat Format of the intro text
* @param int $assessed The forum rating aggregate
* @param int $assesstimestart Timestamp to begin assessment
* @param int $assesstimefinish Timestamp to end assessment
* @param int $scale The rating scale
* @param int $gradeforum The holistic grade
* @param bool $gradeforumnotify Default for whether to notify students when grade holistically
* @param int $maxbytes Maximum attachment size
* @param int $maxattachments Maximum number of attachments
* @param int $forcesubscribe Does the forum force users to subscribe?
* @param int $trackingtype Tracking type
* @param int $rsstype RSS type
* @param int $rssarticles RSS articles
* @param int $timemodified Timestamp when the forum was last modified
* @param int $warnafter Warn after
* @param int $blockafter Block after
* @param int $blockperiod Block period
* @param int $completiondiscussions Completion discussions
* @param int $completionreplies Completion replies
* @param int $completionposts Completion posts
* @param bool $displaywordcount Should display word counts in posts
* @param int $lockdiscussionafter Timestamp after which discussions should be locked
* @param int $duedate Timestamp that represents the due date for forum posts
* @param int $cutoffdate Timestamp after which forum posts will no longer be accepted
*/
public function __construct(
context $context,
stdClass $coursemodule,
stdClass $course,
int $effectivegroupmode,
int $id,
int $courseid,
string $type,
string $name,
string $intro,
int $introformat,
int $assessed,
int $assesstimestart,
int $assesstimefinish,
int $scale,
int $gradeforum,
bool $gradeforumnotify,
int $maxbytes,
int $maxattachments,
int $forcesubscribe,
int $trackingtype,
int $rsstype,
int $rssarticles,
int $timemodified,
int $warnafter,
int $blockafter,
int $blockperiod,
int $completiondiscussions,
int $completionreplies,
int $completionposts,
bool $displaywordcount,
int $lockdiscussionafter,
int $duedate,
int $cutoffdate
) {
$this->context = $context;
$this->coursemodule = $coursemodule;
$this->course = $course;
$this->effectivegroupmode = $effectivegroupmode;
$this->id = $id;
$this->courseid = $courseid;
$this->type = $type;
$this->name = $name;
$this->intro = $intro;
$this->introformat = $introformat;
$this->assessed = $assessed;
$this->assesstimestart = $assesstimestart;
$this->assesstimefinish = $assesstimefinish;
$this->scale = $scale;
$this->gradeforum = $gradeforum;
$this->gradeforumnotify = $gradeforumnotify;
$this->maxbytes = $maxbytes;
$this->maxattachments = $maxattachments;
$this->forcesubscribe = $forcesubscribe;
$this->trackingtype = $trackingtype;
$this->rsstype = $rsstype;
$this->rssarticles = $rssarticles;
$this->timemodified = $timemodified;
$this->warnafter = $warnafter;
$this->blockafter = $blockafter;
$this->blockperiod = $blockperiod;
$this->completiondiscussions = $completiondiscussions;
$this->completionreplies = $completionreplies;
$this->completionposts = $completionposts;
$this->displaywordcount = $displaywordcount;
$this->lockdiscussionafter = $lockdiscussionafter;
$this->duedate = $duedate;
$this->cutoffdate = $cutoffdate;
}
/**
* Get the forum module context.
*
* @return context
*/
public function get_context(): context {
return $this->context;
}
/**
* Get the forum course module record
*
* @return stdClass
*/
public function get_course_module_record(): stdClass {
return $this->coursemodule;
}
/**
* Get the effective group mode.
*
* @return int
*/
public function get_effective_group_mode(): int {
return $this->effectivegroupmode;
}
/**
* Check if the forum is set to group mode.
*
* @return bool
*/
public function is_in_group_mode(): bool {
return $this->get_effective_group_mode() !== NOGROUPS;
}
/**
* Get the course record.
*
* @return stdClass
*/
public function get_course_record(): stdClass {
return $this->course;
}
/**
* Get the forum id.
*
* @return int
*/
public function get_id(): int {
return $this->id;
}
/**
* Get the id of the course that the forum belongs to.
*
* @return int
*/
public function get_course_id(): int {
return $this->courseid;
}
/**
* Get the forum type.
*
* @return string
*/
public function get_type(): string {
return $this->type;
}
/**
* Get the forum name.
*
* @return string
*/
public function get_name(): string {
return $this->name;
}
/**
* Get the forum intro text.
*
* @return string
*/
public function get_intro(): string {
return $this->intro;
}
/**
* Get the forum intro text format.
*
* @return int
*/
public function get_intro_format(): int {
return $this->introformat;
}
/**
* Get the rating aggregate.
*
* @return int
*/
public function get_rating_aggregate(): int {
return $this->assessed;
}
/**
* Does the forum have a rating aggregate?
*
* @return bool
*/
public function has_rating_aggregate(): bool {
return $this->get_rating_aggregate() != RATING_AGGREGATE_NONE;
}
/**
* Get the timestamp for when the assessment period begins.
*
* @return int
*/
public function get_assess_time_start(): int {
return $this->assesstimestart;
}
/**
* Get the timestamp for when the assessment period ends.
*
* @return int
*/
public function get_assess_time_finish(): int {
return $this->assesstimefinish;
}
/**
* Get the rating scale.
*
* @return int
*/
public function get_scale(): int {
return $this->scale;
}
/**
* Get the grade for the forum when grading holistically.
*
* @return int
*/
public function get_grade_for_forum(): int {
return $this->gradeforum;
}
/**
* Whether grading is enabled for this item.
*
* @return bool
*/
public function is_grading_enabled(): bool {
return $this->get_grade_for_forum() !== 0;
}
/**
* Get the default for whether the students should be notified when grading holistically.
*
* @return bool
*/
public function should_notify_students_default_when_grade_for_forum(): bool {
return $this->gradeforumnotify;
}
/**
* Get the maximum bytes.
*
* @return int
*/
public function get_max_bytes(): int {
return $this->maxbytes;
}
/**
* Get the maximum number of attachments.
*
* @return int
*/
public function get_max_attachments(): int {
return $this->maxattachments;
}
/**
* Get the subscription mode.
*
* @return int
*/
public function get_subscription_mode(): int {
return $this->forcesubscribe;
}
/**
* Is the subscription mode set to optional.
*
* @return bool
*/
public function is_subscription_optional(): bool {
return $this->get_subscription_mode() === FORUM_CHOOSESUBSCRIBE;
}
/**
* Is the subscription mode set to forced.
*
* @return bool
*/
public function is_subscription_forced(): bool {
return $this->get_subscription_mode() === FORUM_FORCESUBSCRIBE;
}
/**
* Is the subscription mode set to automatic.
*
* @return bool
*/
public function is_subscription_automatic(): bool {
return $this->get_subscription_mode() === FORUM_INITIALSUBSCRIBE;
}
/**
* Is the subscription mode set to disabled.
*
* @return bool
*/
public function is_subscription_disabled(): bool {
return $this->get_subscription_mode() === FORUM_DISALLOWSUBSCRIBE;
}
/**
* Get the tracking type.
*
* @return int
*/
public function get_tracking_type(): int {
return $this->trackingtype;
}
/**
* Get the RSS type.
*
* @return int
*/
public function get_rss_type(): int {
return $this->rsstype;
}
/**
* Get the RSS articles.
*
* @return int
*/
public function get_rss_articles(): int {
return $this->rssarticles;
}
/**
* Get the timestamp for when the forum was last modified.
*
* @return int
*/
public function get_time_modified(): int {
return $this->timemodified;
}
/**
* Get warn after.
*
* @return int
*/
public function get_warn_after(): int {
return $this->warnafter;
}
/**
* Get block after.
*
* @return int
*/
public function get_block_after(): int {
return $this->blockafter;
}
/**
* Get the block period.
*
* @return int
*/
public function get_block_period(): int {
return $this->blockperiod;
}
/**
* Does the forum have blocking enabled?
*
* @return bool
*/
public function has_blocking_enabled(): bool {
return !empty($this->get_block_after()) && !empty($this->get_block_period());
}
/**
* Get the completion discussions.
*
* @return int
*/
public function get_completion_discussions(): int {
return $this->completiondiscussions;
}
/**
* Get the completion replies.
*
* @return int
*/
public function get_completion_replies(): int {
return $this->completionreplies;
}
/**
* Get the completion posts.
*
* @return int
*/
public function get_completion_posts(): int {
return $this->completionposts;
}
/**
* Should the word counts be shown in the posts?
*
* @return bool
*/
public function should_display_word_count(): bool {
return $this->displaywordcount;
}
/**
* Get the timestamp after which the discussion should be locked.
*
* @return int
*/
public function get_lock_discussions_after(): int {
return $this->lockdiscussionafter;
}
/**
* Does the forum have a discussion locking timestamp?
*
* @return bool
*/
public function has_lock_discussions_after(): bool {
return !empty($this->get_lock_discussions_after());
}
/**
* Check whether the discussion is locked based on forum's time based locking criteria
*
* @param discussion_entity $discussion
* @return bool
*/
public function is_discussion_time_locked(discussion_entity $discussion): bool {
if (!$this->has_lock_discussions_after()) {
return false;
}
if ($this->get_type() === 'single') {
// It does not make sense to lock a single discussion forum.
return false;
}
return (($discussion->get_time_modified() + $this->get_lock_discussions_after()) < time());
}
/**
* Get the cutoff date.
*
* @return int
*/
public function get_cutoff_date(): int {
return $this->cutoffdate;
}
/**
* Does the forum have a cutoff date?
*
* @return bool
*/
public function has_cutoff_date(): bool {
return !empty($this->get_cutoff_date());
}
/**
* Is the cutoff date for the forum reached?
*
* @return bool
*/
public function is_cutoff_date_reached(): bool {
if ($this->has_cutoff_date() && ($this->get_cutoff_date() < time())) {
return true;
}
return false;
}
/**
* Get the due date.
*
* @return int
*/
public function get_due_date(): int {
return $this->duedate;
}
/**
* Does the forum have a due date?
*
* @return bool
*/
public function has_due_date(): bool {
return !empty($this->get_due_date());
}
/**
* Is the due date for the forum reached?
*
* @return bool
*/
public function is_due_date_reached(): bool {
if ($this->has_due_date() && ($this->get_due_date() < time())) {
return true;
}
return false;
}
/**
* Is the discussion locked? - Takes into account both discussion settings AND forum's criteria
*
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function is_discussion_locked(discussion_entity $discussion): bool {
if ($discussion->is_locked()) {
return true;
}
return $this->is_discussion_time_locked($discussion);
}
}
+357
View File
@@ -0,0 +1,357 @@
<?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/>.
/**
* Post class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\entities;
defined('MOODLE_INTERNAL') || die();
use stdClass;
/**
* Post class.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post {
/** @var int $id ID */
private $id;
/** @var int $discussionid The id of the discussion this post belongs to */
private $discussionid;
/** @var int $parentid The id of the post that this post is replying to. Zero if it isn't a reply. */
private $parentid;
/** @var int $authorid The id of user who authored the post */
private $authorid;
/** @var int $timecreated Timestamp for when the post was created */
private $timecreated;
/** @var int $timemodified Timestamp for when the post last modified */
private $timemodified;
/** @var bool $mailed If the post has been mailed */
private $mailed;
/** @var string $subject Post subject */
private $subject;
/** @var string $message Post message */
private $message;
/** @var int $messageformat Format of the post message */
private $messageformat;
/** @var bool $messagetrust Is this a trusted message, i.e. created by a trusted user. */
private $messagetrust;
/** @var bool $hasattachments Does the post have attachments */
private $hasattachments;
/** @var int $totalscore Total score */
private $totalscore;
/** @var bool $mailnow Should this post be mailed immediately */
private $mailnow;
/** @var bool $deleted Is the post deleted */
private $deleted;
/** @var int $privatereplyto The user being privately replied to */
private $privatereplyto;
/** @var int $wordcount Number of words in the message */
private $wordcount;
/** @var int $charcount Number of chars in the message */
private $charcount;
/**
* Constructor.
*
* @param int $id ID
* @param int $discussionid The id of the discussion this post belongs to
* @param int $parentid The id of the post that this post is replying to. Zero if it isn't a reply.
* @param int $authorid The id of user who authored the post
* @param int $timecreated Timestamp for when the post was created
* @param int $timemodified Timestamp for when the post last modified
* @param bool $mailed If the post has been mailed
* @param string $subject Post subject
* @param string $message Post message
* @param int $messageformat Format of the post message
* @param bool $messagetrust Is this a trusted message, i.e. created by a trusted user.
* @param bool $hasattachments Does the post have attachments
* @param int $totalscore Total score
* @param bool $mailnow Should this post be mailed immediately
* @param bool $deleted Is the post deleted
* @param int $privatereplyto Which user this reply is intended for in a private reply situation
*/
public function __construct(
int $id,
int $discussionid,
int $parentid,
int $authorid,
int $timecreated,
int $timemodified,
bool $mailed,
string $subject,
string $message,
int $messageformat,
bool $messagetrust,
bool $hasattachments,
int $totalscore,
bool $mailnow,
bool $deleted,
int $privatereplyto,
?int $wordcount,
?int $charcount
) {
$this->id = $id;
$this->discussionid = $discussionid;
$this->parentid = $parentid;
$this->authorid = $authorid;
$this->timecreated = $timecreated;
$this->timemodified = $timemodified;
$this->mailed = $mailed;
$this->subject = $subject;
$this->message = $message;
$this->messageformat = $messageformat;
$this->messagetrust = $messagetrust;
$this->hasattachments = $hasattachments;
$this->totalscore = $totalscore;
$this->mailnow = $mailnow;
$this->deleted = $deleted;
$this->privatereplyto = $privatereplyto;
$this->wordcount = $wordcount;
$this->charcount = $charcount;
}
/**
* Get the post id.
*
* @return int
*/
public function get_id(): int {
return $this->id;
}
/**
* Get the discussion id.
*
* @return int
*/
public function get_discussion_id(): int {
return $this->discussionid;
}
/**
* Get the id of the parent post. Returns zero if this post is not a reply.
*
* @return int
*/
public function get_parent_id(): int {
return $this->parentid;
}
/**
* Does this post have a parent? I.e. is it a reply?
*
* @return bool
*/
public function has_parent(): bool {
return $this->get_parent_id() > 0;
}
/**
* Get the id of the user that authored the post.
*
* @return int
*/
public function get_author_id(): int {
return $this->authorid;
}
/**
* Get the timestamp for when this post was created.
*
* @return int
*/
public function get_time_created(): int {
return $this->timecreated;
}
/**
* Get the timestamp for when this post was last modified.
*
* @return int
*/
public function get_time_modified(): int {
return $this->timemodified;
}
/**
* Has this post been mailed?
*
* @return bool
*/
public function has_been_mailed(): bool {
return $this->mailed;
}
/**
* Get the post subject.
*
* @return string
*/
public function get_subject(): string {
return $this->subject;
}
/**
* Get the post message.
*
* @return string
*/
public function get_message(): string {
return $this->message;
}
/**
* Get the post message format.
*
* @return int
*/
public function get_message_format(): int {
return $this->messageformat;
}
/**
* Is this a trusted message? I.e. was it authored by a trusted user?
*
* @return bool
*/
public function is_message_trusted(): bool {
return $this->messagetrust;
}
/**
* Does this post have attachments?
*
* @return bool
*/
public function has_attachments(): bool {
return $this->hasattachments;
}
/**
* Get the total score.
*
* @return int
*/
public function get_total_score(): int {
return $this->totalscore;
}
/**
* Should this post be mailed now?
*
* @return bool
*/
public function should_mail_now(): bool {
return $this->mailnow;
}
/**
* Is this post deleted?
*
* @return bool
*/
public function is_deleted(): bool {
return $this->deleted;
}
/**
* Is this post private?
*
* @return bool
*/
public function is_private_reply(): bool {
return !empty($this->privatereplyto);
}
/**
* Get the id of the user that this post was intended for.
*
* @return int
*/
public function get_private_reply_recipient_id(): int {
return $this->privatereplyto;
}
/**
* Get the post's age in seconds.
*
* @return int
*/
public function get_age(): int {
return time() - $this->get_time_created();
}
/**
* Check if the given user authored this post.
*
* @param stdClass $user The user to check.
* @return bool
*/
public function is_owned_by_user(stdClass $user): bool {
return $this->get_author_id() == $user->id;
}
/**
* Check if the given post is a private reply intended for the given user.
*
* @param stdClass $user The user to check.
* @return bool
*/
public function is_private_reply_intended_for_user(stdClass $user): bool {
return $this->get_private_reply_recipient_id() == $user->id;
}
/**
* Returns the word count.
*
* @return int|null
*/
public function get_wordcount(): ?int {
return $this->wordcount;
}
/**
* Returns the char count.
*
* @return int|null
*/
public function get_charcount(): ?int {
return $this->charcount;
}
/**
* This methods adds/updates forum posts' word count and char count attributes based on $data->message.
*
* @param \stdClass $record A record ready to be inserted / updated in DB.
* @return void.
*/
public static function add_message_counts(\stdClass $record): void {
if (!empty($record->message)) {
$record->wordcount = count_words($record->message, $record->messageformat);
$record->charcount = count_letters($record->message, $record->messageformat);
}
}
}
@@ -0,0 +1,86 @@
<?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/>.
/**
* Post read receipt collection class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\entities;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\post as post_entity;
use stdClass;
/**
* Post read receipt collection class.
*
* Contains the list of read receipts for posts.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post_read_receipt_collection {
/** @var stdClass[] $receiptsbypostid Receipt records indexed by post id */
private $receiptsbypostid = [];
/**
* Constructor.
*
* @param array $records The list of post read receipt records.
*/
public function __construct(array $records) {
foreach ($records as $record) {
$postid = $record->postid;
if (isset($this->receiptsbypostid[$postid])) {
$this->receiptsbypostid[$postid][] = $record;
} else {
$this->receiptsbypostid[$postid] = [$record];
}
}
}
/**
* Check whether a user has read a post.
*
* @param stdClass $user The user to check
* @param post_entity $post The post to check
* @return bool
*/
public function has_user_read_post(stdClass $user, post_entity $post): bool {
global $CFG;
$isoldpost = ($post->get_time_modified() < (time() - ($CFG->forum_oldpostdays * 24 * 3600)));
if ($isoldpost) {
return true;
}
$receipts = isset($this->receiptsbypostid[$post->get_id()]) ? $this->receiptsbypostid[$post->get_id()] : [];
foreach ($receipts as $receipt) {
if ($receipt->userid == $user->id) {
return true;
}
}
return false;
}
}
+152
View File
@@ -0,0 +1,152 @@
<?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/>.
/**
* Class to sort items.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\entities;
defined('MOODLE_INTERNAL') || die();
/**
* Class to sort lists of items.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sorter {
/** @var callable $getid Function used to get the id from an item */
private $getid;
/** @var callable $getparentid Function used to get the parent id from an item */
private $getparentid;
/**
* Constructor.
*
* Allows the calling code to provide 2 functions to get the id and parent id from
* the list of items it is intended to process.
*
* This allows this class to be composed in numerous different ways to support various
* types of items while keeping the underlying sorting algorithm consistent.
*
* @param callable $getid Function used to get the id from an item
* @param callable $getparentid Function used to get the parent id from an item
*/
public function __construct(callable $getid, callable $getparentid) {
$this->getid = $getid;
$this->getparentid = $getparentid;
}
/**
* Sort a list of items into a parent/child data structure. The resulting data structure
* is a recursive array of arrays where the first element is the parent and the second is
* an array of it's children.
*
* For example
* If we have an array of items A, B, C, and D where D is a child of C, B and C are children
* of A.
*
* This function would sort them into the following:
* [
* [
* A,
* [
* [
* B,
* []
* ],
* [
* C,
* [
* [
* D,
* []
* ]
* ]
* ]
* ]
* ]
* ]
*
* @param array $items The list of items to sort.
* @return array
*/
public function sort_into_children(array $items): array {
$ids = array_reduce($items, function($carry, $item) {
$carry[($this->getid)($item)] = true;
return $carry;
}, []);
// Split out the items into "parents" and "replies" (children). These are unsorted
// at this point.
[$parents, $replies] = array_reduce($items, function($carry, $item) use ($ids) {
$parentid = ($this->getparentid)($item);
if (!empty($ids[$parentid])) {
// This is a child to another item in the list so add it to the children list.
$carry[1][] = $item;
} else {
// This isn't a child to anything in our list so it's a parent.
$carry[0][] = $item;
}
return $carry;
}, [[], []]);
if (empty($replies)) {
return array_map(function($parent) {
return [$parent, []];
}, $parents);
}
// Recurse to sort the replies into the correct nesting.
$sortedreplies = $this->sort_into_children($replies);
// Sort the parents and sorted replies into their matching pairs.
return array_map(function($parent) use ($sortedreplies) {
$parentid = ($this->getid)($parent);
return [
$parent,
array_values(array_filter($sortedreplies, function($replydata) use ($parentid) {
return ($this->getparentid)($replydata[0]) == $parentid;
}))
];
}, $parents);
}
/**
* Take the data structure returned from "sort_into_children" and flatten it back
* into an array. It does a depth first flatten which maintains the reply ordering.
*
* @param array $items Items in the data structure returned by "sort_into_children"
* @return array A flat array.
*/
public function flatten_children(array $items): array {
$result = [];
foreach ($items as [$item, $children]) {
$result[] = $item;
$result = array_merge($result, $this->flatten_children($children));
}
return $result;
}
}
@@ -0,0 +1,222 @@
<?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/>.
/**
* Author exporter.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\exporters;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\author as author_entity;
use mod_forum\local\exporters\group as group_exporter;
use core\external\exporter;
use renderer_base;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* Author exporter.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class author extends exporter {
/** @var author_entity $author Author entity */
private $author;
/** @var int|null $authorcontextid The context id for the author entity */
private $authorcontextid;
/** @var array $authorgroups List of groups that the author belongs to */
private $authorgroups;
/** @var bool $canview Should the author be anonymised? */
private $canview;
/**
* Constructor.
*
* @param author_entity $author The author entity to export
* @param int|null $authorcontextid The context id for the author entity to export (null if the user doesn't have one)
* @param stdClass[] $authorgroups The list of groups that the author belongs to
* @param bool $canview Can the requesting user view this author or should it be anonymised?
* @param array $related The related data for the export.
*/
public function __construct(
author_entity $author,
?int $authorcontextid,
array $authorgroups = [],
bool $canview = true,
array $related = []
) {
$this->author = $author;
$this->authorcontextid = $authorcontextid;
$this->authorgroups = $authorgroups;
$this->canview = $canview;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
return [
'id' => [
'type' => PARAM_INT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'fullname' => [
'type' => PARAM_TEXT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'isdeleted' => [
'type' => PARAM_BOOL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'groups' => [
'multiple' => true,
'optional' => true,
'type' => [
'id' => ['type' => PARAM_INT],
'name' => ['type' => PARAM_TEXT],
'urls' => [
'type' => [
'image' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
]
]
]
]
],
'urls' => [
'type' => [
'profile' => [
'description' => 'The URL for the use profile page',
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'profileimage' => [
'description' => 'The URL for the use profile image',
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
]
]
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$author = $this->author;
$authorcontextid = $this->authorcontextid;
$urlfactory = $this->related['urlfactory'];
$context = $this->related['context'];
$forum = $this->related['forum'];
if ($this->canview) {
if ($author->is_deleted()) {
return [
'id' => $author->get_id(),
'fullname' => get_string('deleteduser', 'mod_forum'),
'isdeleted' => true,
'groups' => [],
'urls' => [
'profile' => ($urlfactory->get_author_profile_url($author, $forum->get_course_id()))->out(false),
'profileimage' => ($urlfactory->get_author_profile_image_url($author, $authorcontextid))->out(false)
]
];
} else {
$groups = array_map(function($group) use ($urlfactory, $context, $output) {
$groupurl = null;
$imageurl = get_group_picture_url($group, $group->courseid, true);
if (course_can_view_participants($context)) {
$groupurl = $urlfactory->get_author_group_url($group);
}
return [
'id' => $group->id,
'name' => format_string($group->name, true, ['context' => $context]),
'urls' => [
'image' => $imageurl ? $imageurl->out(false) : null,
'group' => $groupurl ? $groupurl->out(false) : null
]
];
}, $this->authorgroups);
return [
'id' => $author->get_id(),
'fullname' => $author->get_full_name(),
'isdeleted' => false,
'groups' => $groups,
'urls' => [
'profile' => ($urlfactory->get_author_profile_url($author, $forum->get_course_id()))->out(false),
'profileimage' => ($urlfactory->get_author_profile_image_url($author, $authorcontextid))->out(false)
]
];
}
} else {
// The author should be anonymised.
return [
'id' => null,
'fullname' => get_string('forumauthorhidden', 'mod_forum'),
'isdeleted' => null,
'groups' => [],
'urls' => [
'profile' => null,
'profileimage' => null
]
];
}
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'urlfactory' => 'mod_forum\local\factories\url',
'context' => 'context',
'forum' => 'mod_forum\local\entities\forum',
];
}
}
@@ -0,0 +1,286 @@
<?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/>.
/**
* Discussion exporter class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\exporters;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\exporters\post as post_exporter;
use mod_forum\local\factories\exporter as exporter_factory;
use core\external\exporter;
use renderer_base;
/**
* Discussion exporter class.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion extends exporter {
/** @var discussion_entity $discussion Discussion to export */
private $discussion;
/**
* Constructor.
*
* @param discussion_entity $discussion Discussion to export
* @param array $related The related export data
*/
public function __construct(discussion_entity $discussion, array $related = []) {
$this->discussion = $discussion;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
return [
'id' => ['type' => PARAM_INT],
'forumid' => ['type' => PARAM_INT],
'pinned' => ['type' => PARAM_BOOL],
'locked' => ['type' => PARAM_BOOL],
'istimelocked' => ['type' => PARAM_BOOL],
'name' => ['type' => PARAM_TEXT],
'firstpostid' => ['type' => PARAM_INT],
'group' => [
'optional' => true,
'type' => [
'name' => ['type' => PARAM_TEXT],
'urls' => [
'type' => [
'picture' => [
'optional' => true,
'type' => PARAM_URL,
],
'userlist' => [
'optional' => true,
'type' => PARAM_URL,
],
],
],
],
],
'times' => [
'type' => [
'modified' => ['type' => PARAM_INT],
'start' => ['type' => PARAM_INT],
'end' => ['type' => PARAM_INT],
'locked' => ['type' => PARAM_INT],
],
],
'userstate' => [
'type' => [
'subscribed' => ['type' => PARAM_BOOL],
'favourited' => ['type' => PARAM_BOOL],
],
],
'capabilities' => [
'type' => [
'subscribe' => ['type' => PARAM_BOOL],
'move' => ['type' => PARAM_BOOL],
'pin' => ['type' => PARAM_BOOL],
'post' => ['type' => PARAM_BOOL],
'manage' => ['type' => PARAM_BOOL],
'favourite' => ['type' => PARAM_BOOL]
]
],
'urls' => [
'type' => [
'view' => ['type' => PARAM_URL],
'viewlatest' => [
'optional' => true,
'type' => PARAM_URL
],
'viewfirstunread' => [
'optional' => true,
'type' => PARAM_URL,
],
'markasread' => ['type' => PARAM_URL],
'subscribe' => ['type' => PARAM_URL],
'pin' => [
'optional' => true,
'type' => PARAM_URL,
],
],
],
'timed' => [
'type' => [
'istimed' => [
'type' => PARAM_BOOL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'visible' => [
'type' => PARAM_BOOL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
]
]
]
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$capabilitymanager = $this->related['capabilitymanager'];
$urlfactory = $this->related['urlfactory'];
$favouriteids = isset($this->related['favouriteids']) ? $this->related['favouriteids'] : [];
$forum = $this->related['forum'];
$forumrecord = $this->get_forum_record();
$user = $this->related['user'];
$discussion = $this->discussion;
$groupdata = null;
if ($discussion->has_group()) {
$groupsbyid = $this->related['groupsbyid'];
$group = $groupsbyid[$discussion->get_group_id()] ?? null;
// We may not have received the group if the caller doesn't want to include it in the export
// or if it's been deleted and the discussion record hasn't been updated.
if ($group) {
$groupdata = [
'name' => format_string($group->name, true, ['context' => $this->related['context']]),
'urls' => [],
];
// If not hiding the group picture, and the group has a picture then use it. Fallback to generic group image.
if ($url = get_group_picture_url($group, $forum->get_course_id(), true)) {
$groupdata['urls']['picture'] = $url;
} else {
$groupdata['urls']['picture'] = $output->image_url('g/g1')->out(false);
}
if ($capabilitymanager->can_view_participants($user, $discussion)) {
$groupdata['urls']['userlist'] = (new \moodle_url('/user/index.php', [
'id' => $forum->get_course_id(),
'group' => $group->id,
]));
}
}
}
$viewfirstunreadurl = $urlfactory->get_discussion_view_first_unread_post_url_from_discussion($discussion);
$data = [
'id' => $discussion->get_id(),
'forumid' => $forum->get_id(),
'pinned' => $discussion->is_pinned(),
'locked' => $forum->is_discussion_locked($discussion),
'istimelocked' => $forum->is_discussion_time_locked($discussion),
'name' => format_string($discussion->get_name(), true, [
'context' => $this->related['context']
]),
'firstpostid' => $discussion->get_first_post_id(),
'times' => [
'modified' => $discussion->get_time_modified(),
'start' => $discussion->get_time_start(),
'end' => $discussion->get_time_end(),
'locked' => $discussion->get_locked()
],
'userstate' => [
'subscribed' => \mod_forum\subscriptions::is_subscribed($user->id, $forumrecord, $discussion->get_id()),
'favourited' => in_array($discussion->get_id(), $favouriteids) ? true : false,
],
'capabilities' => [
'subscribe' => $capabilitymanager->can_subscribe_to_discussion($user, $discussion),
'move' => $capabilitymanager->can_move_discussion($user, $discussion),
'pin' => $capabilitymanager->can_pin_discussion($user, $discussion),
'post' => $capabilitymanager->can_post_in_discussion($user, $discussion),
'manage' => $capabilitymanager->can_manage_forum($user),
'favourite' => $capabilitymanager->can_favourite_discussion($user) // Defaulting to true until we get capabilities sorted
],
'urls' => [
'view' => $urlfactory->get_discussion_view_url_from_discussion($discussion)->out(false),
'viewfirstunread' => $viewfirstunreadurl->out(false),
'markasread' => $urlfactory->get_mark_discussion_as_read_url_from_discussion($forum, $discussion)->out(false),
'subscribe' => $urlfactory->get_discussion_subscribe_url($discussion)->out(false)
]
];
if (!empty($this->related['latestpostid'])) {
$data['urls']['viewlatest'] = $urlfactory->get_discussion_view_latest_post_url_from_discussion(
$discussion,
$this->related['latestpostid']
)->out(false);
}
if ($capabilitymanager->can_pin_discussions($user)) {
$data['urls']['pin'] = $urlfactory->get_pin_discussion_url_from_discussion($discussion)->out(false);
}
if ($groupdata) {
$data['group'] = $groupdata;
}
$canviewhiddentimedposts = $capabilitymanager->can_view_hidden_posts($user);
$canalwaysseetimedpost = $user->id == $discussion->get_user_id() || $canviewhiddentimedposts;
$data['timed']['istimed'] = $canalwaysseetimedpost ? $discussion->is_timed_discussion() : null;
$data['timed']['visible'] = $canalwaysseetimedpost ? $discussion->is_timed_discussion_visible() : null;
return $data;
}
/**
* Get the legacy forum record from the forum entity.
*
* @return stdClass
*/
private function get_forum_record() {
$forumdbdatamapper = $this->related['legacydatamapperfactory']->get_forum_data_mapper();
return $forumdbdatamapper->to_legacy_object($this->related['forum']);
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'legacydatamapperfactory' => 'mod_forum\local\factories\legacy_data_mapper',
'context' => 'context',
'forum' => 'mod_forum\local\entities\forum',
'capabilitymanager' => 'mod_forum\local\managers\capability',
'urlfactory' => 'mod_forum\local\factories\url',
'user' => 'stdClass',
'groupsbyid' => 'stdClass[]',
'latestpostid' => 'int?',
'favouriteids' => 'int[]?'
];
}
}
@@ -0,0 +1,170 @@
<?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/>.
/**
* Discussion summaries exporter.
*
* @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\exporters;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\exporters\post as post_exporter;
use core\external\exporter;
use renderer_base;
/**
* Discussion summaries exporter.
*
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion_summaries extends exporter {
/** @var discussion_summary_entity[] The list of discussion summaries to export */
private $discussions;
/** @var stdClass[] The group information for each author */
private $groupsbyid;
/** @var stdClass[] The group information for each author */
private $groupsbyauthorid;
/** @var int[] Discussion reply counts indexed by dicussion id */
private $discussionreplycount;
/** @var int[] Discussion unread counts indexed by dicussion id */
private $discussionunreadcount;
/** @var array The latest post in each discussion */
private $latestpostids;
/** @var int[] The context ids for the first and latest post authors (indexed by author id) */
private $postauthorcontextids;
/**
* Constructor.
*
* @param discussion_summary_entity[] $discussion The list of discussion summaries to export
* @param stdClass[] $groupsbyid The group information for each author
* @param stdClass[] $groupsbyauthorid The group information for each author
* @param int[] $discussionreplycount Discussion reply counts indexed by dicussion id
* @param int[] $discussionunreadcount Discussion unread counts indexed by dicussion id
* @param int[] $latestpostids List of latest post ids indexed by discussion id
* @param int[] $postauthorcontextids The context ids for the first and latest post authors (indexed by author id)
* @param array $related The related
*/
public function __construct(
array $discussions,
array $groupsbyid,
array $groupsbyauthorid,
array $discussionreplycount,
array $discussionunreadcount,
array $latestpostids,
array $postauthorcontextids,
array $related = []
) {
$this->discussions = $discussions;
$this->groupsbyid = $groupsbyid;
$this->groupsbyauthorid = $groupsbyauthorid;
$this->discussionreplycount = $discussionreplycount;
$this->discussionunreadcount = $discussionunreadcount;
$this->latestpostids = $latestpostids;
$this->postauthorcontextids = $postauthorcontextids;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
return [
'summaries' => [
'type' => discussion_summary::read_properties_definition(),
'multiple' => true
],
'state' => [
'type' => [
'hasdiscussions' => ['type' => PARAM_BOOL],
],
],
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$exporteddiscussions = [];
$related = $this->related;
$latestauthors = $this->related['latestauthors'];
foreach ($this->discussions as $discussion) {
$discussionid = $discussion->get_discussion()->get_id();
$replycount = isset($this->discussionreplycount[$discussionid]) ? $this->discussionreplycount[$discussionid] : 0;
$unreadcount = isset($this->discussionunreadcount[$discussionid]) ? $this->discussionunreadcount[$discussionid] : 0;
$latestpostid = isset($this->latestpostids[$discussionid]) ? $this->latestpostids[$discussionid] : 0;
$latestauthor = $latestauthors[$discussionid] ?? null;
$related['latestauthor'] = $latestauthor;
$exporter = new discussion_summary(
$discussion,
$this->groupsbyid,
$this->groupsbyauthorid,
$replycount,
$unreadcount,
$latestpostid,
$this->postauthorcontextids[$discussion->get_first_post_author()->get_id()],
$this->postauthorcontextids[$latestauthor->get_id()],
$related
);
$exporteddiscussions[] = $exporter->export($output);
}
return [
'summaries' => $exporteddiscussions,
'state' => [
'hasdiscussions' => !empty($exporteddiscussions),
],
];
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'legacydatamapperfactory' => 'mod_forum\local\factories\legacy_data_mapper',
'context' => 'context',
'forum' => 'mod_forum\local\entities\forum',
'capabilitymanager' => 'mod_forum\local\managers\capability',
'urlfactory' => 'mod_forum\local\factories\url',
'user' => 'stdClass',
'favouriteids' => 'int[]?',
'latestauthors' => 'mod_forum\local\entities\author[]?'
];
}
}
@@ -0,0 +1,198 @@
<?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/>.
/**
* Discussion summary exporter class.
*
* @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\exporters;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion_summary as discussion_summary_entity;
use core\external\exporter;
use renderer_base;
/**
* Discussion summary exporter class.
*
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion_summary extends exporter {
/** @var discussion_summary_entity The discussion summary information */
private $summary;
/** @var stdClass[] The group information for each author */
private $groupsbyid;
/** @var stdClass[] The group information for each author */
private $groupsbyauthorid;
/** @var int The number of replies to the discussion */
private $replycount;
/** @var int number of unread posts if the user is tracking these */
private $unreadcount;
/** @var int The latest post id in the discussion */
private $latestpostid;
/** @var int|null The context id for the author of the first post */
private $firstpostauthorcontextid;
/** @var int|null The context id for the author of the latest post */
private $latestpostauthorcontextid;
/**
* Constructor.
*
* @param discussion_summary_entity $summary The discussion summary information
* @param stdClass[] $groupsbyid The group information for each author
* @param stdClass[] $groupsbyauthorid The group information for each author
* @param int $replycount The number of replies to the discussion
* @param int $unreadcount number of unread posts if the user is tracking these
* @param int $latestpostid The latest post id in the discussion
* @param int|null $firstpostauthorcontextid The context id for the author of the first post
* @param int|null $latestpostauthorcontextid The context id for the author of the latest post
* @param array $related The related objects
*/
public function __construct(
discussion_summary_entity $summary,
array $groupsbyid,
array $groupsbyauthorid,
int $replycount,
int $unreadcount,
int $latestpostid,
?int $firstpostauthorcontextid,
?int $latestpostauthorcontextid,
array $related = []
) {
$this->summary = $summary;
$this->groupsbyid = $groupsbyid;
$this->groupsbyauthorid = $groupsbyauthorid;
$this->replycount = $replycount;
$this->unreadcount = $unreadcount;
$this->latestpostid = $latestpostid;
$this->firstpostauthorcontextid = $firstpostauthorcontextid;
$this->latestpostauthorcontextid = $latestpostauthorcontextid;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
return [
'id' => ['type' => PARAM_INT],
'discussion' => [
'type' => discussion::read_properties_definition(),
],
'replies' => ['type' => PARAM_INT],
'unread' => ['type' => PARAM_INT],
'firstpostauthor' => [
'type' => author::read_properties_definition(),
],
'latestpostauthor' => [
'type' => author::read_properties_definition(),
],
'latestpostid' => ['type' => PARAM_INT],
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$capabilitymanager = $this->related['capabilitymanager'];
$forum = $this->related['forum'];
$user = $this->related['user'];
$latestpostauthor = $this->related['latestauthor'];
$discussion = $this->summary->get_discussion();
$related = (array) (object) $this->related;
$related['latestpostid'] = $this->latestpostid;
$related['groupsbyid'] = $this->groupsbyid;
$discussionexporter = new discussion($discussion, $related);
$related = [
'urlfactory' => $this->related['urlfactory'],
'context' => $this->related['forum']->get_context(),
'forum' => $forum,
];
$firstpostauthor = new author(
$this->summary->get_first_post_author(),
$this->firstpostauthorcontextid,
$this->groupsbyauthorid[$this->summary->get_first_post_author()->get_id()],
$capabilitymanager->can_view_post(
$user,
$discussion,
$this->summary->get_first_post()
),
$related
);
$latestpostauthor = new author(
$latestpostauthor ?? $this->summary->get_latest_post_author(),
$this->latestpostauthorcontextid,
[],
$capabilitymanager->can_view_post(
$user,
$discussion,
$this->summary->get_first_post()
),
$related
);
return [
'id' => $discussion->get_id(),
'discussion' => $discussionexporter->export($output),
'replies' => $this->replycount,
'unread' => $this->unreadcount,
'firstpostauthor' => $firstpostauthor->export($output),
'latestpostauthor' => $latestpostauthor->export($output),
'latestpostid' => $this->latestpostid,
];
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'legacydatamapperfactory' => 'mod_forum\local\factories\legacy_data_mapper',
'context' => 'context',
'forum' => 'mod_forum\local\entities\forum',
'capabilitymanager' => 'mod_forum\local\managers\capability',
'urlfactory' => 'mod_forum\local\factories\url',
'user' => 'stdClass',
'favouriteids' => 'int[]?',
'latestauthor' => 'mod_forum\local\entities\author?'
];
}
}
+194
View File
@@ -0,0 +1,194 @@
<?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/>.
/**
* Forum Exporter.
*
* @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\exporters;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\exporters\post as post_exporter;
use core\external\exporter;
use renderer_base;
use stdClass;
/**
* Forum class.
*
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class forum extends exporter {
/** @var forum_entity The entity relating to the forum being displayed */
private $forum;
/**
* Constructor for the forum exporter.
*
* @param forum_entity $forum The forum being displayed
* @param array $related The related objects
*/
public function __construct(forum_entity $forum, $related = []) {
$this->forum = $forum;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
return [
'id' => ['type' => PARAM_INT],
'name' => ['type' => PARAM_RAW],
'state' => [
'type' => [
'groupmode' => ['type' => PARAM_INT],
'gradingenabled' => ['type' => PARAM_BOOL],
],
],
'userstate' => [
'type' => [
'tracked' => ['type' => PARAM_INT],
],
],
'capabilities' => [
'type' => [
'viewdiscussions' => ['type' => PARAM_BOOL],
'create' => ['type' => PARAM_BOOL],
'subscribe' => ['type' => PARAM_BOOL],
'grade' => ['type' => PARAM_BOOL],
]
],
'urls' => [
'type' => [
'create' => ['type' => PARAM_URL],
'markasread' => ['type' => PARAM_URL],
'view' => ['type' => PARAM_URL],
'sortrepliesasc' => ['type' => PARAM_URL],
'sortrepliesdesc' => ['type' => PARAM_URL],
'sortlastpostasc' => ['type' => PARAM_URL],
'sortlastpostdesc' => ['type' => PARAM_URL],
'sortcreatedasc' => ['type' => PARAM_URL],
'sortcreateddesc' => ['type' => PARAM_URL],
'sortdiscussionasc' => ['type' => PARAM_URL],
'sortdiscussiondesc' => ['type' => PARAM_URL],
'sortstarterasc' => ['type' => PARAM_URL],
'sortstarterdesc' => ['type' => PARAM_URL],
'sortgroupasc' => ['type' => PARAM_URL],
'sortgroupdesc' => ['type' => PARAM_URL],
],
],
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$capabilitymanager = $this->related['capabilitymanager'];
$urlfactory = $this->related['urlfactory'];
$user = $this->related['user'];
$currentgroup = $this->related['currentgroup'];
$vaultfactory = $this->related['vaultfactory'];
$discussionvault = $vaultfactory->get_discussions_in_forum_vault();
return [
'id' => $this->forum->get_id(),
'name' => $this->forum->get_name(),
'state' => [
'groupmode' => $this->forum->get_effective_group_mode(),
'gradingenabled' => $this->forum->is_grading_enabled()
],
'userstate' => [
'tracked' => forum_tp_is_tracked($this->get_forum_record(), $this->related['user']),
],
'capabilities' => [
'viewdiscussions' => $capabilitymanager->can_view_discussions($user),
'create' => $capabilitymanager->can_create_discussions($user, $currentgroup),
'selfenrol' => $capabilitymanager->can_self_enrol($user),
'subscribe' => $capabilitymanager->can_subscribe_to_forum($user),
'grade' => $capabilitymanager->can_grade($user),
],
'urls' => [
'create' => $urlfactory->get_discussion_create_url($this->forum)->out(false),
'markasread' => $urlfactory->get_mark_all_discussions_as_read_url($this->forum)->out(false),
'view' => $urlfactory->get_forum_view_url_from_forum($this->forum)->out(false),
'sortrepliesasc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null,
$discussionvault::SORTORDER_REPLIES_ASC)->out(false),
'sortrepliesdesc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null,
$discussionvault::SORTORDER_REPLIES_DESC)->out(false),
'sortlastpostasc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null,
$discussionvault::SORTORDER_LASTPOST_ASC)->out(false),
'sortlastpostdesc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null,
$discussionvault::SORTORDER_LASTPOST_DESC)->out(false),
'sortcreatedasc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null,
$discussionvault::SORTORDER_CREATED_ASC)->out(false),
'sortcreateddesc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null,
$discussionvault::SORTORDER_CREATED_DESC)->out(false),
'sortdiscussionasc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null,
$discussionvault::SORTORDER_DISCUSSION_ASC)->out(false),
'sortdiscussiondesc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null,
$discussionvault::SORTORDER_DISCUSSION_DESC)->out(false),
'sortstarterasc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null,
$discussionvault::SORTORDER_STARTER_ASC)->out(false),
'sortstarterdesc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null,
$discussionvault::SORTORDER_STARTER_DESC)->out(false),
'sortgroupasc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null,
$discussionvault::SORTORDER_GROUP_ASC)->out(false),
'sortgroupdesc' => $urlfactory->get_forum_view_url_from_forum($this->forum, null,
$discussionvault::SORTORDER_GROUP_DESC)->out(false),
],
];
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'legacydatamapperfactory' => 'mod_forum\local\factories\legacy_data_mapper',
'capabilitymanager' => 'mod_forum\local\managers\capability',
'urlfactory' => 'mod_forum\local\factories\url',
'user' => 'stdClass',
'currentgroup' => 'int?',
'vaultfactory' => 'mod_forum\local\factories\vault'
];
}
/**
* Get the legacy forum record for this forum.
*
* @return stdClass
*/
private function get_forum_record(): stdClass {
$forumdbdatamapper = $this->related['legacydatamapperfactory']->get_forum_data_mapper();
return $forumdbdatamapper->to_legacy_object($this->forum);
}
}
+109
View File
@@ -0,0 +1,109 @@
<?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/>.
/**
* Course Group exporter.
*
* @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\exporters;
defined('MOODLE_INTERNAL') || die();
use core\external\exporter;
use renderer_base;
use stdClass;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* Group exporter.
*
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class group extends exporter {
/** @var stdClass $group Group */
private $group;
/**
* Constructor.
*
* @param stdClass $group The group to export
* @param array $related The related data for the export.
*/
public function __construct(stdClass $group, array $related = []) {
$this->group = $group;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
return [
'id' => [
'type' => PARAM_INT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'urls' => [
'type' => [
'image' => [
'description' => 'The URL for the group image',
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
]
],
],
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
return [
'id' => $group->id,
'urls' => [
'image' => $imageurl ? $imageurl->out(false) : null
]
];
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'urlmanager' => 'mod_forum\local\managers\url',
'context' => 'context'
];
}
}
+703
View File
@@ -0,0 +1,703 @@
<?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/>.
/**
* Post exporter class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\exporters;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\exporters\author as author_exporter;
use mod_forum\local\factories\exporter as exporter_factory;
use core\external\exporter;
use core_files\external\stored_file_exporter;
use context;
use core_tag_tag;
use renderer_base;
use stdClass;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* Post exporter class.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post extends exporter {
/** @var post_entity $post The post to export */
private $post;
/**
* Constructor.
*
* @param post_entity $post The post to export
* @param array $related List of related data
*/
public function __construct(post_entity $post, array $related = []) {
$this->post = $post;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
$attachmentdefinition = stored_file_exporter::read_properties_definition();
$attachmentdefinition['urls'] = [
'type' => [
'export' => [
'type' => PARAM_URL,
'description' => 'The URL used to export the attachment',
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
]
]
];
$attachmentdefinition['html'] = [
'type' => [
'plagiarism' => [
'type' => PARAM_RAW,
'description' => 'The HTML source for the Plagiarism Response',
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
]
];
return [
'id' => ['type' => PARAM_INT],
'subject' => ['type' => PARAM_TEXT],
'replysubject' => ['type' => PARAM_TEXT],
'message' => ['type' => PARAM_RAW],
'messageformat' => ['type' => PARAM_INT],
'author' => ['type' => author_exporter::read_properties_definition()],
'discussionid' => ['type' => PARAM_INT],
'hasparent' => ['type' => PARAM_BOOL],
'parentid' => [
'type' => PARAM_INT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'timecreated' => [
'type' => PARAM_INT,
'default' => null,
'null' => NULL_ALLOWED
],
'timemodified' => [
'type' => PARAM_INT,
'default' => null,
'null' => NULL_ALLOWED
],
'unread' => [
'type' => PARAM_BOOL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'isdeleted' => ['type' => PARAM_BOOL],
'isprivatereply' => ['type' => PARAM_BOOL],
'haswordcount' => ['type' => PARAM_BOOL],
'wordcount' => [
'type' => PARAM_INT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'charcount' => [
'type' => PARAM_INT,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'capabilities' => [
'type' => [
'view' => [
'type' => PARAM_BOOL,
'null' => NULL_ALLOWED,
'description' => 'Whether the user can view the post',
],
'edit' => [
'type' => PARAM_BOOL,
'null' => NULL_ALLOWED,
'description' => 'Whether the user can edit the post',
],
'delete' => [
'type' => PARAM_BOOL,
'null' => NULL_ALLOWED,
'description' => 'Whether the user can delete the post',
],
'split' => [
'type' => PARAM_BOOL,
'null' => NULL_ALLOWED,
'description' => 'Whether the user can split the post',
],
'reply' => [
'type' => PARAM_BOOL,
'null' => NULL_ALLOWED,
'description' => 'Whether the user can reply to the post',
],
'selfenrol' => [
'type' => PARAM_BOOL,
'null' => NULL_ALLOWED,
'description' => 'Whether the user can self enrol into the course',
],
'export' => [
'type' => PARAM_BOOL,
'null' => NULL_ALLOWED,
'description' => 'Whether the user can export the post',
],
'controlreadstatus' => [
'type' => PARAM_BOOL,
'null' => NULL_ALLOWED,
'description' => 'Whether the user can control the read status of the post',
],
'canreplyprivately' => [
'type' => PARAM_BOOL,
'null' => NULL_ALLOWED,
'description' => 'Whether the user can post a private reply',
]
]
],
'urls' => [
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'type' => [
'view' => [
'description' => 'The URL used to view the post',
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'viewisolated' => [
'description' => 'The URL used to view the post in isolation',
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'viewparent' => [
'description' => 'The URL used to view the parent of the post',
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'edit' => [
'description' => 'The URL used to edit the post',
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'delete' => [
'description' => 'The URL used to delete the post',
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'split' => [
'description' => 'The URL used to split the discussion ' .
'with the selected post being the first post in the new discussion',
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'reply' => [
'description' => 'The URL used to reply to the post',
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'export' => [
'description' => 'The URL used to export the post',
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'markasread' => [
'description' => 'The URL used to mark the post as read',
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'markasunread' => [
'description' => 'The URL used to mark the post as unread',
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
],
'discuss' => [
'type' => PARAM_URL,
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
]
]
],
'attachments' => [
'multiple' => true,
'type' => $attachmentdefinition
],
'messageinlinefiles' => [
'optional' => true,
'multiple' => true,
'type' => stored_file_exporter::read_properties_definition(),
],
'tags' => [
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'multiple' => true,
'type' => [
'id' => [
'type' => PARAM_INT,
'description' => 'The ID of the Tag',
'null' => NULL_NOT_ALLOWED,
],
'tagid' => [
'type' => PARAM_INT,
'description' => 'The tagid',
'null' => NULL_NOT_ALLOWED,
],
'isstandard' => [
'type' => PARAM_BOOL,
'description' => 'Whether this is a standard tag',
'null' => NULL_NOT_ALLOWED,
],
'displayname' => [
'type' => PARAM_TEXT,
'description' => 'The display name of the tag',
'null' => NULL_NOT_ALLOWED,
],
'flag' => [
'type' => PARAM_BOOL,
'description' => 'Wehther this tag is flagged',
'null' => NULL_NOT_ALLOWED,
],
'urls' => [
'description' => 'URLs associated with the tag',
'null' => NULL_NOT_ALLOWED,
'type' => [
'view' => [
'type' => PARAM_URL,
'description' => 'The URL to view the tag',
'null' => NULL_NOT_ALLOWED,
],
]
]
]
],
'html' => [
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'type' => [
'rating' => [
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'type' => PARAM_RAW,
'description' => 'The HTML source to rate the post',
],
'taglist' => [
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'type' => PARAM_RAW,
'description' => 'The HTML source to view the list of tags',
],
'authorsubheading' => [
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'type' => PARAM_RAW,
'description' => 'The HTML source to view the author details',
],
]
]
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$post = $this->post;
$authorgroups = $this->related['authorgroups'];
$forum = $this->related['forum'];
$discussion = $this->related['discussion'];
$author = $this->related['author'];
$authorcontextid = $this->related['authorcontextid'];
$user = $this->related['user'];
$readreceiptcollection = $this->related['readreceiptcollection'];
$rating = $this->related['rating'];
$tags = $this->related['tags'];
$attachments = $this->related['attachments'];
$inlineattachments = $this->related['messageinlinefiles'];
$includehtml = $this->related['includehtml'];
$isdeleted = $post->is_deleted();
$isprivatereply = $post->is_private_reply();
$hasrating = $rating != null;
$hastags = !empty($tags);
$discussionid = $post->get_discussion_id();
$parentid = $post->get_parent_id();
$capabilitymanager = $this->related['capabilitymanager'];
$canview = $capabilitymanager->can_view_post($user, $discussion, $post);
$canedit = $capabilitymanager->can_edit_post($user, $discussion, $post);
$candelete = $capabilitymanager->can_delete_post($user, $discussion, $post);
$cansplit = $capabilitymanager->can_split_post($user, $discussion, $post);
$canreply = $capabilitymanager->can_reply_to_post($user, $discussion, $post);
$canexport = $capabilitymanager->can_export_post($user, $post);
$cancontrolreadstatus = $capabilitymanager->can_manually_control_post_read_status($user);
$canselfenrol = $capabilitymanager->can_self_enrol($user);
$canreplyprivately = $capabilitymanager->can_reply_privately_to_post($user, $post);
$urlfactory = $this->related['urlfactory'];
$viewurl = $canview ? $urlfactory->get_view_post_url_from_post($post) : null;
$viewisolatedurl = $canview ? $urlfactory->get_view_isolated_post_url_from_post($post) : null;
$viewparenturl = $post->has_parent() ? $urlfactory->get_view_post_url_from_post_id($discussionid, $parentid) : null;
$editurl = $canedit ? $urlfactory->get_edit_post_url_from_post($forum, $post) : null;
$deleteurl = $candelete ? $urlfactory->get_delete_post_url_from_post($post) : null;
$spliturl = $cansplit ? $urlfactory->get_split_discussion_at_post_url_from_post($post) : null;
$replyurl = $canreply || $canselfenrol ? $urlfactory->get_reply_to_post_url_from_post($post) : null;
$exporturl = $canexport ? $urlfactory->get_export_post_url_from_post($post) : null;
$markasreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_read_url_from_post($post) : null;
$markasunreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_unread_url_from_post($post) : null;
$discussurl = $canview ? $urlfactory->get_discussion_view_url_from_post($post) : null;
$authorexporter = new author_exporter(
$author,
$authorcontextid,
$authorgroups,
$canview,
$this->related
);
$exportedauthor = $authorexporter->export($output);
// Only bother loading the content if the user can see it.
$loadcontent = $canview && !$isdeleted;
$exportattachments = $loadcontent && !empty($attachments);
$exportinlineattachments = $loadcontent && !empty($inlineattachments);
if ($loadcontent) {
$subject = $post->get_subject();
$timecreated = $this->get_start_time($discussion, $post);
$message = $this->get_message($post);
} else {
$subject = $isdeleted ? get_string('forumsubjectdeleted', 'forum') : get_string('forumsubjecthidden', 'forum');
$message = $isdeleted ? get_string('forumbodydeleted', 'forum') : get_string('forumbodyhidden', 'forum');
$timecreated = null;
}
$replysubject = $subject;
$strre = get_string('re', 'forum');
if (!(substr($replysubject, 0, strlen($strre)) == $strre)) {
$replysubject = "{$strre} {$replysubject}";
}
$showwordcount = $forum->should_display_word_count();
if ($showwordcount) {
$wordcount = $post->get_wordcount() ?? count_words($message);
$charcount = $post->get_charcount() ?? count_letters($message);
} else {
$wordcount = null;
$charcount = null;
}
return [
'id' => $post->get_id(),
'subject' => $subject,
'replysubject' => $replysubject,
'message' => $message,
'messageformat' => $post->get_message_format(),
'author' => $exportedauthor,
'discussionid' => $post->get_discussion_id(),
'hasparent' => $post->has_parent(),
'parentid' => $post->has_parent() ? $post->get_parent_id() : null,
'timecreated' => $timecreated,
'timemodified' => $post->get_time_modified(),
'unread' => ($loadcontent && $readreceiptcollection) ? !$readreceiptcollection->has_user_read_post($user, $post) : null,
'isdeleted' => $isdeleted,
'isprivatereply' => $isprivatereply,
'haswordcount' => $showwordcount,
'wordcount' => $wordcount,
'charcount' => $charcount,
'capabilities' => [
'view' => $canview,
'edit' => $canedit,
'delete' => $candelete,
'split' => $cansplit,
'reply' => $canreply,
'export' => $canexport,
'controlreadstatus' => $cancontrolreadstatus,
'canreplyprivately' => $canreplyprivately,
'selfenrol' => $canselfenrol
],
'urls' => [
'view' => $viewurl ? $viewurl->out(false) : null,
'viewisolated' => $viewisolatedurl ? $viewisolatedurl->out(false) : null,
'viewparent' => $viewparenturl ? $viewparenturl->out(false) : null,
'edit' => $editurl ? $editurl->out(false) : null,
'delete' => $deleteurl ? $deleteurl->out(false) : null,
'split' => $spliturl ? $spliturl->out(false) : null,
'reply' => $replyurl ? $replyurl->out(false) : null,
'export' => $exporturl && $exporturl ? $exporturl->out(false) : null,
'markasread' => $markasreadurl ? $markasreadurl->out(false) : null,
'markasunread' => $markasunreadurl ? $markasunreadurl->out(false) : null,
'discuss' => $discussurl ? $discussurl->out(false) : null,
],
'attachments' => ($exportattachments) ? $this->export_attachments($attachments, $post, $output, $canexport) : [],
'messageinlinefiles' => ($exportinlineattachments) ? $this->export_inline_attachments($inlineattachments,
$post, $output) : [],
'tags' => ($loadcontent && $hastags) ? $this->export_tags($tags) : [],
'html' => $includehtml ? [
'rating' => ($loadcontent && $hasrating) ? $output->render($rating) : null,
'taglist' => ($loadcontent && $hastags) ? $output->tag_list($tags) : null,
'authorsubheading' => ($loadcontent) ? $this->get_author_subheading_html($exportedauthor, $timecreated) : null
] : null
];
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'capabilitymanager' => 'mod_forum\local\managers\capability',
'readreceiptcollection' => 'mod_forum\local\entities\post_read_receipt_collection?',
'urlfactory' => 'mod_forum\local\factories\url',
'forum' => 'mod_forum\local\entities\forum',
'discussion' => 'mod_forum\local\entities\discussion',
'author' => 'mod_forum\local\entities\author',
'authorcontextid' => 'int?',
'user' => 'stdClass',
'context' => 'context',
'authorgroups' => 'stdClass[]',
'attachments' => '\stored_file[]?',
'messageinlinefiles' => '\stored_file[]?',
'tags' => '\core_tag_tag[]?',
'rating' => 'rating?',
'includehtml' => 'bool'
];
}
/**
* This method returns the parameters for the post's message to
* use with the function \core_external\util::format_text().
*
* @return array
*/
protected function get_format_parameters_for_message() {
return [
'component' => 'mod_forum',
'filearea' => 'post',
'itemid' => $this->post->get_id(),
'options' => [
'para' => false,
'trusted' => $this->post->is_message_trusted()
]
];
}
/**
* Get the message text from a post.
*
* @param post_entity $post The post
* @return string
*/
private function get_message(post_entity $post): string {
global $CFG;
$message = $post->get_message();
if (!empty($CFG->enableplagiarism)) {
require_once($CFG->libdir . '/plagiarismlib.php');
$forum = $this->related['forum'];
$message .= plagiarism_get_links([
'userid' => $post->get_author_id(),
'content' => $message,
'cmid' => $forum->get_course_module_record()->id,
'course' => $forum->get_course_id(),
'forum' => $forum->get_id()
]);
}
return $message;
}
/**
* Get the exported attachments for a post.
*
* @param stored_file[] $attachments The list of attachments for the post
* @param post_entity $post The post being exported
* @param renderer_base $output Renderer base
* @param bool $canexport If the user can export the post (relates to portfolios not exporters like this class)
* @return array
*/
private function export_attachments(array $attachments, post_entity $post, renderer_base $output, bool $canexport): array {
global $CFG;
$urlfactory = $this->related['urlfactory'];
$enableplagiarism = $CFG->enableplagiarism;
$forum = $this->related['forum'];
$context = $this->related['context'];
if ($enableplagiarism) {
require_once($CFG->libdir . '/plagiarismlib.php' );
}
return array_map(function($attachment) use (
$output,
$enableplagiarism,
$canexport,
$context,
$forum,
$post,
$urlfactory
) {
$exporter = new stored_file_exporter($attachment, ['context' => $context]);
$exportedattachment = $exporter->export($output);
$exporturl = $canexport ? $urlfactory->get_export_attachment_url_from_post_and_attachment($post, $attachment) : null;
if ($enableplagiarism) {
$plagiarismhtml = plagiarism_get_links([
'userid' => $post->get_author_id(),
'file' => $attachment,
'cmid' => $forum->get_course_module_record()->id,
'course' => $forum->get_course_id(),
'forum' => $forum->get_id()
]);
} else {
$plagiarismhtml = null;
}
$exportedattachment->urls = [
'export' => $exporturl ? $exporturl->out(false) : null
];
$exportedattachment->html = [
'plagiarism' => $plagiarismhtml
];
return $exportedattachment;
}, $attachments);
}
/**
* Get the exported inline attachments for a post.
*
* @param array $inlineattachments The list of inline attachments for the post
* @param post_entity $post The post being exported
* @param renderer_base $output Renderer base
* @return array
*/
private function export_inline_attachments(array $inlineattachments, post_entity $post, renderer_base $output): array {
return array_map(function($attachment) use (
$output,
$post
) {
$exporter = new stored_file_exporter($attachment, ['context' => $this->related['context']]);
return $exporter->export($output);;
}, $inlineattachments);
}
/**
* Export the list of tags.
*
* @param core_tag_tag[] $tags List of tags to export
* @return array
*/
private function export_tags(array $tags): array {
$user = $this->related['user'];
$context = $this->related['context'];
$capabilitymanager = $this->related['capabilitymanager'];
$canmanagetags = $capabilitymanager->can_manage_tags($user);
return array_values(array_map(function($tag) use ($context, $canmanagetags) {
$viewurl = core_tag_tag::make_url($tag->tagcollid, $tag->rawname, 0, $context->id);
return [
'id' => $tag->taginstanceid,
'tagid' => $tag->id,
'isstandard' => $tag->isstandard,
'displayname' => $tag->get_display_name(),
'flag' => $canmanagetags && !empty($tag->flag),
'urls' => [
'view' => $viewurl->out(false)
]
];
}, $tags));
}
/**
* Get the HTML to display as a subheading in a post.
*
* @param stdClass $exportedauthor The exported author object
* @param int $timecreated The post time created timestamp if it's to be displayed
* @return string
*/
private function get_author_subheading_html(stdClass $exportedauthor, int $timecreated): string {
$fullname = $exportedauthor->fullname;
$profileurl = $exportedauthor->urls['profile'] ?? null;
$name = $profileurl ? "<a href=\"{$profileurl}\">{$fullname}</a>" : $fullname;
$date = userdate_htmltime($timecreated, get_string('strftimedaydatetime', 'core_langconfig'));
return get_string('bynameondate', 'mod_forum', ['name' => $name, 'date' => $date]);
}
/**
* Get the start time for a post.
*
* @param discussion_entity $discussion entity
* @param post_entity $post entity
* @return int The start time (timestamp) for a post
*/
private function get_start_time(discussion_entity $discussion, post_entity $post) {
global $CFG;
$posttime = $post->get_time_created();
$discussiontime = $discussion->get_time_start();
if (!empty($CFG->forum_enabletimedposts) && ($discussiontime > $posttime)) {
return $discussiontime;
}
return $posttime;
}
}
+182
View File
@@ -0,0 +1,182 @@
<?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/>.
/**
* Posts exporter class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\exporters;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\author as author_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\exporters\post as post_exporter;
use core\external\exporter;
use renderer_base;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* Posts exporter class.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class posts extends exporter {
/** @var post_entity[] $posts List of posts to export */
private $posts;
/** @var author_entity[] $authorsbyid List of authors for the posts indexed by author id */
private $authorsbyid;
/** @var int[] $authorcontextids List of authors context ids indexed by author id */
private $authorcontextids;
/** @var array $attachmentsbypostid List of attachments indexed by post id */
private $attachmentsbypostid;
/** @var array $inlineattachmentsbypostid List of inline attachments indexed by post id */
private $inlineattachmentsbypostid;
/** @var array $groupsbyauthorid List of author's groups indexed by author id */
private $groupsbyauthorid;
/** @var array $tagsbypostid List of tags indexed by post id */
private $tagsbypostid;
/** @var array $ratingbypostid List of ratings indexed by post id */
private $ratingbypostid;
/**
* Constructor.
*
* @param post_entity[] $posts List of posts to export
* @param author_entity[] $authorsbyid List of authors for the posts indexed by author id
* @param int[] $authorcontextids List of authors context ids indexed by author id
* @param array $attachmentsbypostid List of attachments indexed by post id
* @param array $groupsbyauthorid List of author's groups indexed by author id
* @param array $tagsbypostid List of tags indexed by post id
* @param array $ratingbypostid List of ratings indexed by post id
* @param array $related The related objects for exporting
* @param array $inlineattachmentsbypostid List of inline attachments indexed by post id
*/
public function __construct(
array $posts,
array $authorsbyid = [],
array $authorcontextids = [],
array $attachmentsbypostid = [],
array $groupsbyauthorid = [],
array $tagsbypostid = [],
array $ratingbypostid = [],
array $related = [],
array $inlineattachmentsbypostid = []
) {
$this->posts = $posts;
$this->authorsbyid = $authorsbyid;
$this->authorcontextids = $authorcontextids;
$this->attachmentsbypostid = $attachmentsbypostid;
$this->inlineattachmentsbypostid = $inlineattachmentsbypostid;
$this->groupsbyauthorid = $groupsbyauthorid;
$this->tagsbypostid = $tagsbypostid;
$this->ratingbypostid = $ratingbypostid;
return parent::__construct([], $related);
}
/**
* Return the list of additional properties.
*
* @return array
*/
protected static function define_other_properties() {
return [
'posts' => [
'type' => post_exporter::read_properties_definition(),
'multiple' => true
]
];
}
/**
* Get the additional values to inject while exporting.
*
* @param renderer_base $output The renderer.
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
$related = $this->related;
$authorsbyid = $this->authorsbyid;
$authorcontextids = $this->authorcontextids;
$attachmentsbypostid = $this->attachmentsbypostid;
$inlineattachmentsbypostid = $this->inlineattachmentsbypostid;
$groupsbyauthorid = $this->groupsbyauthorid;
$tagsbypostid = $this->tagsbypostid;
$ratingbypostid = $this->ratingbypostid;
$exportedposts = array_map(
function($post) use (
$related,
$authorsbyid,
$authorcontextids,
$attachmentsbypostid,
$groupsbyauthorid,
$tagsbypostid,
$ratingbypostid,
$output,
$inlineattachmentsbypostid
) {
$authorid = $post->get_author_id();
$postid = $post->get_id();
$author = isset($authorsbyid[$authorid]) ? $authorsbyid[$authorid] : [];
$authorcontextid = isset($authorcontextids[$authorid]) ? $authorcontextids[$authorid] : null;
$attachments = isset($attachmentsbypostid[$postid]) ? $attachmentsbypostid[$postid] : [];
$inlineattachments = isset($inlineattachmentsbypostid[$postid]) ? $inlineattachmentsbypostid[$postid] : [];
$authorgroups = isset($groupsbyauthorid[$authorid]) ? $groupsbyauthorid[$authorid] : [];
$tags = isset($tagsbypostid[$postid]) ? $tagsbypostid[$postid] : [];
$rating = isset($ratingbypostid[$postid]) ? $ratingbypostid[$postid] : null;
$exporter = new post_exporter($post, array_merge($related, [
'author' => $author,
'authorcontextid' => $authorcontextid,
'attachments' => $attachments,
'messageinlinefiles' => $inlineattachments,
'authorgroups' => $authorgroups,
'tags' => $tags,
'rating' => $rating
]));
return $exporter->export($output);
},
$this->posts
);
return [
'posts' => $exportedposts
];
}
/**
* Returns a list of objects that are related.
*
* @return array
*/
protected static function define_related() {
return [
'capabilitymanager' => 'mod_forum\local\managers\capability',
'urlfactory' => 'mod_forum\local\factories\url',
'forum' => 'mod_forum\local\entities\forum',
'discussion' => 'mod_forum\local\entities\discussion',
'readreceiptcollection' => 'mod_forum\local\entities\post_read_receipt_collection?',
'user' => 'stdClass',
'context' => 'context',
'includehtml' => 'bool'
];
}
}
@@ -0,0 +1,127 @@
<?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/>.
/**
* Builder factory.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\builders\exported_posts as exported_posts_builder;
use mod_forum\local\builders\exported_discussion_summaries as exported_discussion_summaries_builder;
use mod_forum\local\builders\exported_discussion as exported_discussion_builder;
use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\manager as manager_factory;
use renderer_base;
/**
* Builder factory to construct any builders for forum.
*
* See:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class builder {
/** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */
private $legacydatamapperfactory;
/** @var exporter_factory $exporterfactory Exporter factory */
private $exporterfactory;
/** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory;
/** @var manager_factory $managerfactory Manager factory */
private $managerfactory;
/** @var renderer_base $rendererbase Renderer base */
private $rendererbase;
/**
* Constructor.
*
* @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
* @param exporter_factory $exporterfactory Exporter factory
* @param vault_factory $vaultfactory Vault factory
* @param manager_factory $managerfactory Manager factory
* @param renderer_base $rendererbase Renderer base
*/
public function __construct(
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
manager_factory $managerfactory,
renderer_base $rendererbase
) {
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->managerfactory = $managerfactory;
$this->rendererbase = $rendererbase;
}
/**
* Get an instance of the exported posts builder.
*
* @return exported_posts_builder
*/
public function get_exported_posts_builder(): exported_posts_builder {
return new exported_posts_builder(
$this->rendererbase,
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$this->managerfactory
);
}
/**
* Get an instance of the exported discussion summaries builder.
*
* @return exported_discussion_summaries_builder
*/
public function get_exported_discussion_summaries_builder(): exported_discussion_summaries_builder {
return new exported_discussion_summaries_builder(
$this->rendererbase,
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$this->managerfactory
);
}
/**
* Get an instance of the exported discussion builder.
*
* @return exported_discussion_summaries_builder
*/
public function get_exported_discussion_builder(): exported_discussion_builder {
return new exported_discussion_builder(
$this->rendererbase,
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$this->managerfactory->get_rating_manager()
);
}
}
@@ -0,0 +1,258 @@
<?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/>.
/**
* Entity factory.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\author as author_entity;
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\discussion_summary as discussion_summary_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\entities\post_read_receipt_collection as post_read_receipt_collection_entity;
use mod_forum\local\entities\sorter as sorter_entity;
use stdClass;
use context;
use cm_info;
use user_picture;
use moodle_url;
/**
* Entity factory to create the forum entities.
*
* See:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class entity {
/**
* Create a forum entity from a stdClass (legacy forum object).
*
* @param stdClass $record The forum record
* @param context $context The forum module context
* @param stdClass $coursemodule Course module record for the forum
* @param stdClass $course Course the forum belongs to
* @return forum_entity
*/
public function get_forum_from_stdclass(
stdClass $record,
context $context,
stdClass $coursemodule,
stdClass $course
): forum_entity {
// Note: cm_info::create loads a cm_info in the context of the current user which
// creates hidden dependency on the logged in user (very bad) however it's the best
// option to load some data we need which doesn't require the logged in user.
// Only use properties which do not require the logged in user.
$cm = \cm_info::create($coursemodule);
return new forum_entity(
$context,
$coursemodule,
$course,
// This property is a general module property that isn't affected by the logged in user.
$cm->effectivegroupmode,
$record->id,
$record->course,
$record->type,
$record->name,
$record->intro,
$record->introformat,
$record->assessed,
$record->assesstimestart,
$record->assesstimefinish,
$record->scale,
$record->grade_forum,
$record->grade_forum_notify,
$record->maxbytes,
$record->maxattachments,
$record->forcesubscribe,
$record->trackingtype,
$record->rsstype,
$record->rssarticles,
$record->timemodified,
$record->warnafter,
$record->blockafter,
$record->blockperiod,
$record->completiondiscussions,
$record->completionreplies,
$record->completionposts,
$record->displaywordcount,
$record->lockdiscussionafter,
$record->duedate,
$record->cutoffdate
);
}
/**
* Create a discussion entity from an stdClass (legacy dicussion object).
*
* @param stdClass $record Discussion record
* @return discussion_entity
*/
public function get_discussion_from_stdclass(stdClass $record): discussion_entity {
return new discussion_entity(
$record->id,
$record->course,
$record->forum,
$record->name,
$record->firstpost,
$record->userid,
$record->groupid,
$record->assessed,
$record->timemodified,
$record->usermodified,
$record->timestart,
$record->timeend,
$record->pinned,
$record->timelocked
);
}
/**
* Create a post entity from an stdClass (legacy post object).
*
* @param stdClass $record The post record
* @return post_entity
*/
public function get_post_from_stdclass(stdClass $record): post_entity {
return new post_entity(
$record->id,
$record->discussion,
$record->parent,
$record->userid,
$record->created,
$record->modified,
$record->mailed,
$record->subject,
$record->message,
$record->messageformat,
$record->messagetrust,
$record->attachment,
$record->totalscore,
$record->mailnow,
$record->deleted,
$record->privatereplyto,
$record->wordcount,
$record->charcount
);
}
/**
* Create an author entity from a user record.
*
* @param stdClass $record The user record
* @return author_entity
*/
public function get_author_from_stdclass(stdClass $record): author_entity {
return new author_entity(
$record->id,
$record->picture,
$record->firstname,
$record->lastname,
fullname($record),
$record->email,
$record->deleted,
$record->middlename,
$record->firstnamephonetic,
$record->lastnamephonetic,
$record->alternatename,
$record->imagealt
);
}
/**
* Create a discussion summary enttiy from stdClasses.
*
* @param stdClass $discussion The discussion record
* @param stdClass $firstpost A post record for the first post in the discussion
* @param stdClass $firstpostauthor A user record for the author of the first post
* @param stdClass $latestpostauthor A user record for the author of the latest post in the discussion
* @return discussion_summary_entity
*/
public function get_discussion_summary_from_stdclass(
stdClass $discussion,
stdClass $firstpost,
stdClass $firstpostauthor,
stdClass $latestpostauthor
): discussion_summary_entity {
$firstpostauthorentity = $this->get_author_from_stdclass($firstpostauthor);
return new discussion_summary_entity(
$this->get_discussion_from_stdclass($discussion),
$this->get_post_from_stdclass($firstpost, $firstpostauthorentity),
$firstpostauthorentity,
$this->get_author_from_stdclass($latestpostauthor)
);
}
/**
* Create a post read receipt collection entity from a list of read receipt records.
*
* @param array $records A list of read receipt records.
* @return post_read_receipt_collection_entity
*/
public function get_post_read_receipt_collection_from_stdclasses(array $records): post_read_receipt_collection_entity {
return new post_read_receipt_collection_entity($records);
}
/**
* Create a sorter entity to sort post entities.
*
* @return sorter_entity
*/
public function get_posts_sorter(): sorter_entity {
return new sorter_entity(
// Get id function for a post_entity.
function(post_entity $post) {
return $post->get_id();
},
// Get parent id function for a post_entity.
function(post_entity $post) {
return $post->get_parent_id();
}
);
}
/**
* Create a sorter entity to sort exported posts.
*
* @return sorter_entity
*/
public function get_exported_posts_sorter(): sorter_entity {
return new sorter_entity(
// Get id function for an exported post.
function(stdClass $post) {
return $post->id;
},
// Get parent id function for an exported post.
function(stdClass $post) {
return $post->parentid;
}
);
}
}
@@ -0,0 +1,275 @@
<?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/>.
/**
* Forum Exporter factory.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\entities\post_read_receipt_collection as post_read_receipt_collection_entity;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\manager as manager_factory;
use mod_forum\local\factories\url as url_factory;
use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\exporters\forum as forum_exporter;
use mod_forum\local\exporters\discussion as discussion_exporter;
use mod_forum\local\exporters\discussion_summaries as discussion_summaries_exporter;
use mod_forum\local\exporters\post as post_exporter;
use mod_forum\local\exporters\posts as posts_exporter;
use context;
use rating;
use stdClass;
/**
* The exporter factory class used to fetch an instance of the different exporter types.
*
* See:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class exporter {
/** @var legacy_data_mapper_factory The factory to fetch a legacy data mapper */
private $legacydatamapperfactory;
/** @var manager_factory The factory to fetch a new manager */
private $managerfactory;
/** @var url_factory The factory to create urls */
private $urlfactory;
/** @var vault_factory The vault factory */
private $vaultfactory;
/**
* Constructor for the exporter factory.
*
* @param legacy_data_mapper_factory $legacydatamapperfactory The factory to fetch a legacy data mapper instance
* @param manager_factory $managerfactory The factory fo fetch a manager instance
* @param url_factory $urlfactory The factory to create urls
* @param vault_factory $vaultfactory The vault factory
*/
public function __construct(legacy_data_mapper_factory $legacydatamapperfactory, manager_factory $managerfactory,
url_factory $urlfactory, vault_factory $vaultfactory) {
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->managerfactory = $managerfactory;
$this->urlfactory = $urlfactory;
$this->vaultfactory = $vaultfactory;
}
/**
* Construct a new forum exporter for the specified user and forum.
*
* @param stdClass $user The user viewing the forum
* @param forum_entity $forum The forum being viewed
* @param int $currentgroup The group currently being viewed
* @return forum_exporter
*/
public function get_forum_exporter(
stdClass $user,
forum_entity $forum,
?int $currentgroup
): forum_exporter {
return new forum_exporter($forum, [
'legacydatamapperfactory' => $this->legacydatamapperfactory,
'capabilitymanager' => $this->managerfactory->get_capability_manager($forum),
'urlfactory' => $this->urlfactory,
'user' => $user,
'currentgroup' => $currentgroup,
'vaultfactory' => $this->vaultfactory,
]);
}
/**
* Fetch the structure of the forum exporter.
*
* @return array
*/
public static function get_forum_export_structure(): array {
return forum_exporter::read_properties_definition();
}
/**
* Construct a new discussion exporter for the specified user and forum discussion.
*
* @param stdClass $user The user viewing the forum
* @param forum_entity $forum The forum being viewed
* @param discussion_entity $discussion The discussion being viewed
* @param stdClass[] $groupsbyid The list of groups in the forum
* @return discussion_exporter
*/
public function get_discussion_exporter(
stdClass $user,
forum_entity $forum,
discussion_entity $discussion,
array $groupsbyid = [],
array $favouriteids = []
): discussion_exporter {
return new discussion_exporter($discussion, [
'context' => $forum->get_context(),
'forum' => $forum,
'capabilitymanager' => $this->managerfactory->get_capability_manager($forum),
'urlfactory' => $this->urlfactory,
'user' => $user,
'legacydatamapperfactory' => $this->legacydatamapperfactory,
'latestpostid' => null,
'groupsbyid' => $groupsbyid,
'favouriteids' => $favouriteids
]);
}
/**
* Fetch the structure of the discussion exporter.
*
* @return array
*/
public static function get_discussion_export_structure() {
return discussion_exporter::read_properties_definition();
}
/**
* Construct a new discussion summaries exporter for the specified user and set of discussions.
*
* @param stdClass $user The user viewing the forum
* @param forum_entity $forum The forum being viewed
* @param discussion_summary_entity[] $discussions The set of discussion summaries to export
* @param stdClass[] $groupsbyauthorid The set of groups in an associative array for each author
* @param stdClass[] $groupsbyid The set of groups in the forum in an associative array for each group
* @param int[] $discussionreplycount The number of replies for each discussion
* @param int[] $discussionunreadcount The number of unread posts for each discussion
* @param int[] $latestpostids The latest post id for each discussion
* @param int[] $postauthorcontextids The context ids for the first and last post authors (indexed by author id)
* @param int[] $favourites The list of discussion ids that have been favourited
* @return discussion_summaries_exporter
*/
public function get_discussion_summaries_exporter(
stdClass $user,
forum_entity $forum,
array $discussions,
array $groupsbyid = [],
array $groupsbyauthorid = [],
array $discussionreplycount = [],
array $discussionunreadcount = [],
array $latestpostids = [],
array $postauthorcontextids = [],
array $favourites = [],
array $latestauthors = []
): discussion_summaries_exporter {
return new discussion_summaries_exporter(
$discussions,
$groupsbyid,
$groupsbyauthorid,
$discussionreplycount,
$discussionunreadcount,
$latestpostids,
$postauthorcontextids,
[
'legacydatamapperfactory' => $this->legacydatamapperfactory,
'context' => $forum->get_context(),
'forum' => $forum,
'capabilitymanager' => $this->managerfactory->get_capability_manager($forum),
'urlfactory' => $this->urlfactory,
'user' => $user,
'favouriteids' => $favourites,
'latestauthors' => $latestauthors
]
);
}
/**
* Fetch the structure of the discussion summaries exporter.
*
* @return array
*/
public static function get_discussion_summaries_export_structure() {
return discussion_summaries_exporter::read_properties_definition();
}
/**
* Construct a new post exporter for the specified user and set of post.
*
* @param stdClass $user The user viewing the forum
* @param forum_entity $forum The forum being viewed
* @param discussion_entity $discussion The discussion that the post is in
* @param post_entity[] $posts The set of posts to be exported
* @param author_entity[] $authorsbyid List of authors indexed by author id
* @param int[] $authorcontextids List of authors context ids indexed by author id
* @param array $attachmentsbypostid List of attachments for each post indexed by post id
* @param array $groupsbyauthorid List of groups for the post authors indexed by author id
* @param post_read_receipt_collection_entity|null $readreceiptcollection Details of read receipts for each post
* @param array $tagsbypostid List of tags for each post indexed by post id
* @param rating[] $ratingbypostid List of ratings for each post indexed by post id
* @param bool $includehtml Include some pre-constructed HTML in the export
* @param array $inlineattachmentsbypostid List of attachments for each post indexed by post id
* @return posts_exporter
*/
public function get_posts_exporter(
stdClass $user,
forum_entity $forum,
discussion_entity $discussion,
array $posts,
array $authorsbyid = [],
array $authorcontextids = [],
array $attachmentsbypostid = [],
array $groupsbyauthorid = [],
post_read_receipt_collection_entity $readreceiptcollection = null,
array $tagsbypostid = [],
array $ratingbypostid = [],
bool $includehtml = false,
array $inlineattachmentsbypostid = []
): posts_exporter {
return new posts_exporter(
$posts,
$authorsbyid,
$authorcontextids,
$attachmentsbypostid,
$groupsbyauthorid,
$tagsbypostid,
$ratingbypostid,
[
'capabilitymanager' => $this->managerfactory->get_capability_manager($forum),
'urlfactory' => $this->urlfactory,
'forum' => $forum,
'discussion' => $discussion,
'user' => $user,
'context' => $forum->get_context(),
'readreceiptcollection' => $readreceiptcollection,
'includehtml' => $includehtml
],
$inlineattachmentsbypostid
);
}
/**
* Fetch the structure of the posts exporter.
*
* @return array
*/
public static function get_posts_export_structure() {
return posts_exporter::read_properties_definition();
}
}
@@ -0,0 +1,99 @@
<?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/>.
/**
* Legacy data mapper factory.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\data_mappers\legacy\author as author_data_mapper;
use mod_forum\local\data_mappers\legacy\discussion as discussion_data_mapper;
use mod_forum\local\data_mappers\legacy\forum as forum_data_mapper;
use mod_forum\local\data_mappers\legacy\post as post_data_mapper;
use mod_forum\local\entities\forum;
/**
* Legacy data mapper factory.
*
* See:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class legacy_data_mapper {
/**
* Create a legacy forum data mapper.
*
* @return forum_data_mapper
*/
public function get_forum_data_mapper(): forum_data_mapper {
return new forum_data_mapper();
}
/**
* Create a legacy discussion data mapper.
*
* @return discussion_data_mapper
*/
public function get_discussion_data_mapper(): discussion_data_mapper {
return new discussion_data_mapper();
}
/**
* Create a legacy post data mapper.
*
* @return post_data_mapper
*/
public function get_post_data_mapper(): post_data_mapper {
return new post_data_mapper();
}
/**
* Create a legacy author data mapper.
*
* @return author_data_mapper
*/
public function get_author_data_mapper(): author_data_mapper {
return new author_data_mapper();
}
/**
* Get the corresponding entity based on the supplied value
*
* @param string $entity
* @return author_data_mapper|discussion_data_mapper|forum_data_mapper|post_data_mapper
*/
public function get_legacy_data_mapper_for_vault($entity) {
switch($entity) {
case 'forum':
return $this->get_forum_data_mapper();
case 'discussion':
return $this->get_discussion_data_mapper();
case 'post':
return $this->get_post_data_mapper();
case 'author':
return $this->get_author_data_mapper();
}
}
}
@@ -0,0 +1,81 @@
<?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/>.
/**
* Managers factory.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/rating/lib.php');
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\managers\capability as capability_manager;
use rating_manager;
/**
* Managers factory.
*
* See:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manager {
/** @var legacy_data_mapper $legacydatamapperfactory Legacy data mapper factory */
private $legacydatamapperfactory;
/**
* Constructor.
*
* @param legacy_data_mapper $legacydatamapperfactory Legacy data mapper factory
*/
public function __construct(legacy_data_mapper $legacydatamapperfactory) {
$this->legacydatamapperfactory = $legacydatamapperfactory;
}
/**
* Create a capability manager for the given forum.
*
* @param forum_entity $forum The forum to manage capabilities for
* @return capability_manager
*/
public function get_capability_manager(forum_entity $forum) {
return new capability_manager(
$forum,
$this->legacydatamapperfactory->get_forum_data_mapper(),
$this->legacydatamapperfactory->get_discussion_data_mapper(),
$this->legacydatamapperfactory->get_post_data_mapper()
);
}
/**
* Create a rating manager.
*
* @return rating_manager
*/
public function get_rating_manager(): rating_manager {
return new rating_manager();
}
}
@@ -0,0 +1,638 @@
<?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/>.
/**
* Renderer factory.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
use mod_forum\grades\forum_gradeitem;
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\entity as entity_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\manager as manager_factory;
use mod_forum\local\factories\builder as builder_factory;
use mod_forum\local\factories\url as url_factory;
use mod_forum\local\renderers\discussion as discussion_renderer;
use mod_forum\local\renderers\discussion_list as discussion_list_renderer;
use mod_forum\local\renderers\posts as posts_renderer;
use moodle_page;
use core\output\notification;
/**
* Renderer factory.
*
* See:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class renderer {
/** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */
private $legacydatamapperfactory;
/** @var exporter_factory $exporterfactory Exporter factory */
private $exporterfactory;
/** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory;
/** @var manager_factory $managerfactory Manager factory */
private $managerfactory;
/** @var entity_factory $entityfactory Entity factory */
private $entityfactory;
/** @var builder_factory $builderfactory Builder factory */
private $builderfactory;
/** @var url_factory $urlfactory URL factory */
private $urlfactory;
/** @var renderer_base $rendererbase Renderer base */
private $rendererbase;
/** @var moodle_page $page Moodle page */
private $page;
/**
* Constructor.
*
* @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
* @param exporter_factory $exporterfactory Exporter factory
* @param vault_factory $vaultfactory Vault factory
* @param manager_factory $managerfactory Manager factory
* @param entity_factory $entityfactory Entity factory
* @param builder_factory $builderfactory Builder factory
* @param url_factory $urlfactory URL factory
* @param moodle_page $page Moodle page
*/
public function __construct(
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
manager_factory $managerfactory,
entity_factory $entityfactory,
builder_factory $builderfactory,
url_factory $urlfactory,
moodle_page $page
) {
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->managerfactory = $managerfactory;
$this->entityfactory = $entityfactory;
$this->builderfactory = $builderfactory;
$this->urlfactory = $urlfactory;
$this->page = $page;
$this->rendererbase = $page->get_renderer('mod_forum');
}
/**
* Create a discussion renderer for the given forum and discussion.
*
* @param forum_entity $forum Forum the discussion belongs to
* @param discussion_entity $discussion Discussion to render
* @param int $displaymode How should the posts be formatted?
* @return discussion_renderer
*/
public function get_discussion_renderer(
forum_entity $forum,
discussion_entity $discussion,
int $displaymode
): discussion_renderer {
$capabilitymanager = $this->managerfactory->get_capability_manager($forum);
$ratingmanager = $this->managerfactory->get_rating_manager();
$rendererbase = $this->rendererbase;
$baseurl = $this->urlfactory->get_discussion_view_url_from_discussion($discussion);
$notifications = [];
return new discussion_renderer(
$forum,
$discussion,
$displaymode,
$rendererbase,
$this->get_single_discussion_posts_renderer($displaymode, false),
$this->page,
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$this->urlfactory,
$this->entityfactory,
$capabilitymanager,
$ratingmanager,
$this->entityfactory->get_exported_posts_sorter(),
$baseurl,
$notifications,
function($discussion, $user, $forum) {
$exportbuilder = $this->builderfactory->get_exported_discussion_builder();
return $exportbuilder->build(
$user,
$forum,
$discussion
);
}
);
}
/**
* Create a posts renderer to render posts without defined parent/reply relationships.
*
* @return posts_renderer
*/
public function get_posts_renderer(): posts_renderer {
return new posts_renderer(
$this->rendererbase,
$this->builderfactory->get_exported_posts_builder(),
'mod_forum/forum_discussion_posts'
);
}
/**
* Create a posts renderer to render a list of posts in a single discussion.
*
* @param int|null $displaymode How should the posts be formatted?
* @param bool $readonly Should the posts include the actions to reply, delete, etc?
* @return posts_renderer
*/
public function get_single_discussion_posts_renderer(int $displaymode = null, bool $readonly = false): posts_renderer {
$exportedpostssorter = $this->entityfactory->get_exported_posts_sorter();
switch ($displaymode) {
case FORUM_MODE_THREADED:
$template = 'mod_forum/forum_discussion_threaded_posts';
break;
case FORUM_MODE_NESTED:
$template = 'mod_forum/forum_discussion_nested_posts';
break;
case FORUM_MODE_NESTED_V2:
$template = 'mod_forum/forum_discussion_nested_v2_posts';
break;
default;
$template = 'mod_forum/forum_discussion_posts';
break;
}
return new posts_renderer(
$this->rendererbase,
$this->builderfactory->get_exported_posts_builder(),
$template,
// Post process the exported posts for our template. This function will add the "replies"
// and "hasreplies" properties to the exported posts. It will also sort them into the
// reply tree structure if the display mode requires it.
function($exportedposts, $forums, $discussions) use ($displaymode, $readonly, $exportedpostssorter) {
$forum = array_shift($forums);
$seenfirstunread = false;
$postcount = count($exportedposts);
$discussionsbyid = array_reduce($discussions, function($carry, $discussion) {
$carry[$discussion->get_id()] = $discussion;
return $carry;
}, []);
$exportedposts = array_map(
function($exportedpost) use ($forum, $discussionsbyid, $readonly, $seenfirstunread, $displaymode) {
$discussion = $discussionsbyid[$exportedpost->discussionid] ?? null;
if ($forum->get_type() == 'single' && !$exportedpost->hasparent) {
// Remove the author from any posts that don't have a parent.
unset($exportedpost->author);
unset($exportedpost->html['authorsubheading']);
}
$exportedpost->firstpost = false;
$exportedpost->readonly = $readonly;
$exportedpost->hasreplycount = false;
$exportedpost->hasreplies = false;
$exportedpost->replies = [];
$exportedpost->discussionlocked = $discussion ? $discussion->is_locked() : null;
$exportedpost->isfirstunread = false;
if (!$seenfirstunread && $exportedpost->unread) {
$exportedpost->isfirstunread = true;
$seenfirstunread = true;
}
if ($displaymode === FORUM_MODE_NESTED_V2) {
$exportedpost->showactionmenu = $exportedpost->capabilities['view'] ||
$exportedpost->capabilities['controlreadstatus'] ||
$exportedpost->capabilities['edit'] ||
$exportedpost->capabilities['split'] ||
$exportedpost->capabilities['delete'] ||
$exportedpost->capabilities['export'] ||
!empty($exportedpost->urls['viewparent']);
}
return $exportedpost;
},
$exportedposts
);
if (
$displaymode === FORUM_MODE_NESTED ||
$displaymode === FORUM_MODE_THREADED ||
$displaymode === FORUM_MODE_NESTED_V2
) {
$sortedposts = $exportedpostssorter->sort_into_children($exportedposts);
$sortintoreplies = function($nestedposts) use (&$sortintoreplies) {
return array_map(function($postdata) use (&$sortintoreplies) {
[$post, $replies] = $postdata;
$totalreplycount = 0;
if (empty($replies)) {
$post->replies = [];
$post->hasreplies = false;
} else {
$sortedreplies = $sortintoreplies($replies);
// Set the parent author name on the replies. This is used for screen
// readers to help them identify the structure of the discussion.
$sortedreplies = array_map(function($reply) use ($post) {
if (isset($post->author)) {
$reply->parentauthorname = $post->author->fullname;
} else {
// The only time the author won't be set is for a single discussion
// forum. See above for where it gets unset.
$reply->parentauthorname = get_string('firstpost', 'mod_forum');
}
return $reply;
}, $sortedreplies);
$totalreplycount = array_reduce($sortedreplies, function($carry, $reply) {
return $carry + 1 + $reply->totalreplycount;
}, $totalreplycount);
$post->replies = $sortedreplies;
$post->hasreplies = true;
}
$post->totalreplycount = $totalreplycount;
return $post;
}, $nestedposts);
};
// Set the "replies" property on the exported posts.
$exportedposts = $sortintoreplies($sortedposts);
} else if ($displaymode === FORUM_MODE_FLATNEWEST || $displaymode === FORUM_MODE_FLATOLDEST) {
$exportedfirstpost = array_shift($exportedposts);
$exportedfirstpost->replies = $exportedposts;
$exportedfirstpost->hasreplies = true;
$exportedposts = [$exportedfirstpost];
}
if (!empty($exportedposts)) {
// Need to identify the first post so that we can use it in behat tests.
$exportedposts[0]->firstpost = true;
$exportedposts[0]->hasreplycount = true;
$exportedposts[0]->replycount = $postcount - 1;
}
return $exportedposts;
}
);
}
/**
* Create a posts renderer to render posts in the forum search results.
*
* @param string[] $searchterms The search terms to be highlighted in the posts
* @return posts_renderer
*/
public function get_posts_search_results_renderer(array $searchterms): posts_renderer {
$urlfactory = $this->urlfactory;
return new posts_renderer(
$this->rendererbase,
$this->builderfactory->get_exported_posts_builder(),
'mod_forum/forum_search_results',
// Post process the exported posts to add the highlighting of the search terms to the post
// and also the additional context links in the subject.
function($exportedposts, $forumsbyid, $discussionsbyid) use ($searchterms, $urlfactory) {
$highlightwords = implode(' ', $searchterms);
return array_map(
function($exportedpost) use (
$forumsbyid,
$discussionsbyid,
$searchterms,
$highlightwords,
$urlfactory
) {
$discussion = $discussionsbyid[$exportedpost->discussionid];
$forum = $forumsbyid[$discussion->get_forum_id()];
$viewdiscussionurl = $urlfactory->get_discussion_view_url_from_discussion($discussion);
$exportedpost->urls['viewforum'] = $urlfactory->get_forum_view_url_from_forum($forum)->out(false);
$exportedpost->urls['viewdiscussion'] = $viewdiscussionurl->out(false);
$exportedpost->subject = highlight($highlightwords, $exportedpost->subject);
$exportedpost->forumname = format_string($forum->get_name(), true);
$exportedpost->discussionname = highlight($highlightwords, format_string($discussion->get_name(), true));
$exportedpost->showdiscussionname = $forum->get_type() != 'single';
// Identify search terms only found in HTML markup, and add a warning about them to
// the start of the message text. This logic was copied exactly as is from the previous
// implementation.
$missingterms = '';
$exportedpost->message = highlight(
$highlightwords,
$exportedpost->message,
0,
'<fgw9sdpq4>',
'</fgw9sdpq4>'
);
foreach ($searchterms as $searchterm) {
if (
preg_match("/$searchterm/i", $exportedpost->message) &&
!preg_match('/<fgw9sdpq4>' . $searchterm . '<\/fgw9sdpq4>/i', $exportedpost->message)
) {
$missingterms .= " $searchterm";
}
}
$exportedpost->message = str_replace('<fgw9sdpq4>', '<span class="highlight">', $exportedpost->message);
$exportedpost->message = str_replace('</fgw9sdpq4>', '</span>', $exportedpost->message);
if ($missingterms) {
$strmissingsearchterms = get_string('missingsearchterms', 'forum');
$exportedpost->message = '<p class="highlight2">' . $strmissingsearchterms . ' '
. $missingterms . '</p>' . $exportedpost->message;
}
return $exportedpost;
},
$exportedposts
);
}
);
}
/**
* Create a posts renderer to render posts in mod/forum/user.php.
*
* @param bool $addlinkstocontext Should links to the course, forum, and discussion be included?
* @return posts_renderer
*/
public function get_user_forum_posts_report_renderer(bool $addlinkstocontext): posts_renderer {
$urlfactory = $this->urlfactory;
return new posts_renderer(
$this->rendererbase,
$this->builderfactory->get_exported_posts_builder(),
'mod_forum/forum_posts_with_context_links',
function($exportedposts, $forumsbyid, $discussionsbyid) use ($urlfactory, $addlinkstocontext) {
return array_map(function($exportedpost) use ($forumsbyid, $discussionsbyid, $addlinkstocontext, $urlfactory) {
$discussion = $discussionsbyid[$exportedpost->discussionid];
$forum = $forumsbyid[$discussion->get_forum_id()];
$courserecord = $forum->get_course_record();
if ($addlinkstocontext) {
$viewdiscussionurl = $urlfactory->get_discussion_view_url_from_discussion($discussion);
$exportedpost->urls['viewforum'] = $urlfactory->get_forum_view_url_from_forum($forum)->out(false);
$exportedpost->urls['viewdiscussion'] = $viewdiscussionurl->out(false);
$exportedpost->urls['viewcourse'] = $urlfactory->get_course_url_from_forum($forum)->out(false);
}
$exportedpost->forumname = format_string($forum->get_name(), true);
$exportedpost->discussionname = format_string($discussion->get_name(), true);
$exportedpost->coursename = format_string($courserecord->shortname, true);
$exportedpost->showdiscussionname = $forum->get_type() != 'single';
return $exportedpost;
}, $exportedposts);
}
);
}
/**
* Create a standard type discussion list renderer.
*
* @param forum_entity $forum The forum that the discussions belong to
* @return discussion_list_renderer
*/
public function get_discussion_list_renderer(
forum_entity $forum
): discussion_list_renderer {
$capabilitymanager = $this->managerfactory->get_capability_manager($forum);
$rendererbase = $this->rendererbase;
$notifications = [];
switch ($forum->get_type()) {
case 'news':
if (SITEID == $forum->get_course_id()) {
$template = 'mod_forum/frontpage_news_discussion_list';
} else {
$template = 'mod_forum/news_discussion_list';
}
break;
case 'qanda':
$template = 'mod_forum/qanda_discussion_list';
break;
default:
$template = 'mod_forum/discussion_list';
}
return new discussion_list_renderer(
$forum,
$rendererbase,
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$this->builderfactory,
$capabilitymanager,
$this->urlfactory,
forum_gradeitem::load_from_forum_entity($forum),
$template,
$notifications,
function($discussions, $user, $forum) {
$exporteddiscussionsummarybuilder = $this->builderfactory->get_exported_discussion_summaries_builder();
return $exporteddiscussionsummarybuilder->build(
$user,
$forum,
$discussions
);
}
);
}
/**
* Create a discussion list renderer which shows more information about the first post.
*
* @param forum_entity $forum The forum that the discussions belong to
* @param string $template The template to use
* @return discussion_list_renderer
*/
private function get_detailed_discussion_list_renderer(
forum_entity $forum,
string $template
): discussion_list_renderer {
$capabilitymanager = $this->managerfactory->get_capability_manager($forum);
$rendererbase = $this->rendererbase;
$notifications = [];
return new discussion_list_renderer(
$forum,
$rendererbase,
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$this->builderfactory,
$capabilitymanager,
$this->urlfactory,
forum_gradeitem::load_from_forum_entity($forum),
$template,
$notifications,
function($discussions, $user, $forum) use ($capabilitymanager) {
$exportedpostsbuilder = $this->builderfactory->get_exported_posts_builder();
$discussionentries = [];
$postentries = [];
foreach ($discussions as $discussion) {
$discussionentries[] = $discussion->get_discussion();
$discussionentriesids[] = $discussion->get_discussion()->get_id();
$postentries[] = $discussion->get_first_post();
}
$exportedposts['posts'] = $exportedpostsbuilder->build(
$user,
[$forum],
$discussionentries,
$postentries
);
$postvault = $this->vaultfactory->get_post_vault();
$canseeanyprivatereply = $capabilitymanager->can_view_any_private_reply($user);
$discussionrepliescount = $postvault->get_reply_count_for_discussion_ids(
$user,
$discussionentriesids,
$canseeanyprivatereply
);
$forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper();
$forumrecord = $forumdatamapper->to_legacy_object($forum);
if (forum_tp_is_tracked($forumrecord, $user)) {
$discussionunreadscount = $postvault->get_unread_count_for_discussion_ids(
$user,
$discussionentriesids,
$canseeanyprivatereply
);
} else {
$discussionunreadscount = [];
}
array_walk($exportedposts['posts'], function($post) use ($discussionrepliescount, $discussionunreadscount) {
$post->discussionrepliescount = $discussionrepliescount[$post->discussionid] ?? 0;
$post->discussionunreadscount = $discussionunreadscount[$post->discussionid] ?? 0;
// TODO: Find a better solution due to language differences when defining the singular and plural form.
$post->isreplyplural = $post->discussionrepliescount != 1 ? true : false;
$post->isunreadplural = $post->discussionunreadscount != 1 ? true : false;
});
$exportedposts['state']['hasdiscussions'] = $exportedposts['posts'] ? true : false;
return $exportedposts;
}
);
}
/**
* Create a blog type discussion list renderer.
*
* @param forum_entity $forum The forum that the discussions belong to
* @return discussion_list_renderer
*/
public function get_blog_discussion_list_renderer(
forum_entity $forum
): discussion_list_renderer {
return $this->get_detailed_discussion_list_renderer($forum, 'mod_forum/blog_discussion_list');
}
/**
* Create a discussion list renderer for the social course format.
*
* @param forum_entity $forum The forum that the discussions belong to
* @return discussion_list_renderer
*/
public function get_social_discussion_list_renderer(
forum_entity $forum
): discussion_list_renderer {
return $this->get_detailed_discussion_list_renderer($forum, 'mod_forum/social_discussion_list');
}
/**
* Create a discussion list renderer for the social course format.
*
* @param forum_entity $forum The forum that the discussions belong to
* @return discussion_list_renderer
*/
public function get_frontpage_news_discussion_list_renderer(
forum_entity $forum
): discussion_list_renderer {
return $this->get_detailed_discussion_list_renderer($forum, 'mod_forum/frontpage_social_discussion_list');
}
/**
* Create a single type discussion list renderer.
*
* @param forum_entity $forum Forum the discussion belongs to
* @param discussion_entity $discussion The discussion entity
* @param bool $hasmultiplediscussions Whether the forum has multiple discussions (more than one)
* @param int $displaymode How should the posts be formatted?
* @return discussion_renderer
*/
public function get_single_discussion_list_renderer(
forum_entity $forum,
discussion_entity $discussion,
bool $hasmultiplediscussions,
int $displaymode
): discussion_renderer {
$capabilitymanager = $this->managerfactory->get_capability_manager($forum);
$ratingmanager = $this->managerfactory->get_rating_manager();
$rendererbase = $this->rendererbase;
$cmid = $forum->get_course_module_record()->id;
$baseurl = $this->urlfactory->get_forum_view_url_from_course_module_id($cmid);
$notifications = array();
if ($hasmultiplediscussions) {
$notifications[] = (new notification(get_string('warnformorepost', 'forum')))
->set_show_closebutton(true);
}
return new discussion_renderer(
$forum,
$discussion,
$displaymode,
$rendererbase,
$this->get_single_discussion_posts_renderer($displaymode, false),
$this->page,
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$this->urlfactory,
$this->entityfactory,
$capabilitymanager,
$ratingmanager,
$this->entityfactory->get_exported_posts_sorter(),
$baseurl,
$notifications
);
}
}
+507
View File
@@ -0,0 +1,507 @@
<?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/>.
/**
* A URL factory for the forum.
*
* @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\factories;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\author as author_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use moodle_url;
use stored_file;
use user_picture;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* A URL factory for the forum.
*
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class url {
/** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */
private $legacydatamapperfactory;
/**
* Constructor.
*
* @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
*/
public function __construct(legacy_data_mapper_factory $legacydatamapperfactory) {
$this->legacydatamapperfactory = $legacydatamapperfactory;
}
/**
* Get the course url from the given course id.
*
* @param int $courseid The course id
* @return moodle_url
*/
public function get_course_url_from_courseid(int $courseid): moodle_url {
return new moodle_url('/course/view.php', [
'id' => $courseid,
]);
}
/**
* Get the course url from the given forum entity.
*
* @param forum_entity $forum The forum entity
* @return moodle_url
*/
public function get_course_url_from_forum(forum_entity $forum): moodle_url {
return $this->get_course_url_from_courseid($forum->get_course_id());
}
/**
* Get the create discussion url for the given forum.
*
* @param forum_entity $forum The forum entity
* @return moodle_url
*/
public function get_discussion_create_url(forum_entity $forum): moodle_url {
return new moodle_url('/mod/forum/post.php', [
'forum' => $forum->get_id(),
]);
}
/**
* Get the view forum url for the given forum and optionally a page number.
*
* @param forum_entity $forum The forum entity
* @param int|null $pageno The page number
* @param int|null $sortorder The sorting order
* @return moodle_url
*/
public function get_forum_view_url_from_forum(forum_entity $forum, ?int $pageno = null,
?int $sortorder = null): moodle_url {
return $this->get_forum_view_url_from_course_module_id($forum->get_course_module_record()->id, $pageno, $sortorder);
}
/**
* Get the view forum url for the given course module id and optionally a page number.
*
* @param int $coursemoduleid The course module id
* @param int|null $pageno The page number
* @param int|null $sortorder The sorting order
* @return moodle_url
*/
public function get_forum_view_url_from_course_module_id(int $coursemoduleid, ?int $pageno = null,
?int $sortorder = null): moodle_url {
$url = new moodle_url('/mod/forum/view.php', [
'id' => $coursemoduleid,
]);
if (null !== $pageno) {
$url->param('p', $pageno);
}
if (null !== $sortorder) {
$url->param('o', $sortorder);
}
return $url;
}
/**
* Get the view discussion url from the given discussion id.
*
* @param int $discussionid The discussion id
* @return moodle_url
*/
public function get_discussion_view_url_from_discussion_id(int $discussionid): moodle_url {
return new moodle_url('/mod/forum/discuss.php', [
'd' => $discussionid
]);
}
/**
* Get the view discussion url from the given discussion.
*
* @param discussion_entity $discussion The discussion
* @return moodle_url
*/
public function get_discussion_view_url_from_discussion(discussion_entity $discussion): moodle_url {
return $this->get_discussion_view_url_from_discussion_id($discussion->get_id());
}
/**
* Get the url to view the first unread post in a discussion.
*
* @param discussion_entity $discussion The discussion
* @return moodle_url
*/
public function get_discussion_view_first_unread_post_url_from_discussion(discussion_entity $discussion) {
$viewurl = $this->get_discussion_view_url_from_discussion_id($discussion->get_id());
$viewurl->set_anchor('unread');
return $viewurl;
}
/**
* Get the url to view the latest post in a discussion.
*
* @param discussion_entity $discussion The discussion
* @param int|null $latestpost The id of the latest post
* @return moodle_url
*/
public function get_discussion_view_latest_post_url_from_discussion(discussion_entity $discussion, ?int $latestpost) {
$viewurl = $this->get_discussion_view_url_from_discussion_id($discussion->get_id());
if (null === $latestpost) {
return $viewurl;
} else {
return new moodle_url($viewurl, ['parent' => $latestpost]);
}
}
/**
* Get the url to view a discussion from a post.
*
* @param post_entity $post The post
* @return moodle_url
*/
public function get_discussion_view_url_from_post(post_entity $post): moodle_url {
return $this->get_discussion_view_url_from_discussion_id($post->get_discussion_id());
}
/**
* Get the url to view a discussion from a discussion id and post id.
*
* @param int $discussionid The discussion id
* @param int $postid The post id
* @return moodle_url
*/
public function get_view_post_url_from_post_id(int $discussionid, int $postid): moodle_url {
$url = $this->get_discussion_view_url_from_discussion_id($discussionid);
$url->set_anchor('p' . $postid);
return $url;
}
/**
* Get the url to view a post in the context of the rest of the discussion.
*
* @param post_entity $post The post
* @return moodle_url
*/
public function get_view_post_url_from_post(post_entity $post): moodle_url {
return $this->get_view_post_url_from_post_id($post->get_discussion_id(), $post->get_id());
}
/**
* Get the url to view a post and it's replies in isolation without the rest of the
* discussion.
*
* @param int $discussionid The discussion id
* @param int $postid The post id
* @return moodle_url
*/
public function get_view_isolated_post_url_from_post_id(int $discussionid, int $postid): moodle_url {
$url = $this->get_discussion_view_url_from_discussion_id($discussionid);
$url->params(['parent' => $postid]);
return $url;
}
/**
* Get the url to view a post and it's replies in isolation without the rest of the
* discussion.
*
* @param post_entity $post The post
* @return moodle_url
*/
public function get_view_isolated_post_url_from_post(post_entity $post): moodle_url {
return $this->get_view_isolated_post_url_from_post_id($post->get_discussion_id(), $post->get_id());
}
/**
* Get the url to edit a post.
*
* @param forum_entity $forum The forum the post belongs to
* @param post_entity $post The post
* @return moodle_url
*/
public function get_edit_post_url_from_post(forum_entity $forum, post_entity $post): moodle_url {
if ($forum->get_type() == 'single' && !$post->has_parent()) {
return new moodle_url('/course/modedit.php', [
'update' => $forum->get_course_module_record()->id,
'sesskey' => sesskey(),
'return' => 1
]);
} else {
return new moodle_url('/mod/forum/post.php', [
'edit' => $post->get_id()
]);
}
}
/**
* Get the url to split a discussion at a post.
*
* @param post_entity $post The post
* @return moodle_url
*/
public function get_split_discussion_at_post_url_from_post(post_entity $post): moodle_url {
return new moodle_url('/mod/forum/post.php', [
'prune' => $post->get_id()
]);
}
/**
* Get the url to delete a post.
*
* @param post_entity $post The post
* @return moodle_url
*/
public function get_delete_post_url_from_post(post_entity $post): moodle_url {
return new moodle_url('/mod/forum/post.php', [
'delete' => $post->get_id()
]);
}
/**
* Get the url to reply to a post.
*
* @param post_entity $post The post
* @return moodle_url
*/
public function get_reply_to_post_url_from_post(post_entity $post): moodle_url {
return new moodle_url('/mod/forum/post.php#mformforum', [
'reply' => $post->get_id()
]);
}
/**
* Get the url to export (see portfolios) a post.
*
* @param post_entity $post The post
* @return moodle_url
*/
public function get_export_post_url_from_post(post_entity $post): ?moodle_url {
global $CFG;
require_once($CFG->libdir . '/portfoliolib.php');
$button = new \portfolio_add_button();
$button->set_callback_options('forum_portfolio_caller', ['postid' => $post->get_id()], 'mod_forum');
if ($post->has_attachments()) {
$button->set_formats(PORTFOLIO_FORMAT_RICHHTML);
} else {
$button->set_formats(PORTFOLIO_FORMAT_PLAINHTML);
}
$url = $button->to_html(PORTFOLIO_ADD_MOODLE_URL);
return $url ?: null;
}
/**
* Get the url to mark a post as read.
*
* @param post_entity $post The post
* @param int $displaymode The display mode to show the forum in after marking as read
* @return moodle_url
*/
public function get_mark_post_as_read_url_from_post(post_entity $post, int $displaymode = FORUM_MODE_THREADED): moodle_url {
$params = [
'd' => $post->get_discussion_id(),
'postid' => $post->get_id(),
'mark' => 'read'
];
$url = new moodle_url('/mod/forum/discuss.php', $params);
if ($displaymode == FORUM_MODE_THREADED) {
$url->param('parent', $post->get_parent_id());
} else {
$url->set_anchor('p' . $post->get_id());
}
return $url;
}
/**
* Get the url to mark a post as unread.
*
* @param post_entity $post The post
* @param int $displaymode The display mode to show the forum in after marking as unread
* @return moodle_url
*/
public function get_mark_post_as_unread_url_from_post(post_entity $post, int $displaymode = FORUM_MODE_THREADED): moodle_url {
$params = [
'd' => $post->get_discussion_id(),
'postid' => $post->get_id(),
'mark' => 'unread'
];
$url = new moodle_url('/mod/forum/discuss.php', $params);
if ($displaymode == FORUM_MODE_THREADED) {
$url->param('parent', $post->get_parent_id());
} else {
$url->set_anchor('p' . $post->get_id());
}
return $url;
}
/**
* Get the url to export attachments for a post.
*
* @param post_entity $post The post
* @param stored_file $attachment
* @return moodle_url|null
*/
public function get_export_attachment_url_from_post_and_attachment(post_entity $post, stored_file $attachment): ?moodle_url {
global $CFG;
require_once($CFG->libdir . '/portfoliolib.php');
$button = new \portfolio_add_button();
$button->set_callback_options(
'forum_portfolio_caller',
['postid' => $post->get_id(), 'attachment' => $attachment->get_id()],
'mod_forum'
);
$button->set_format_by_file($attachment);
$url = $button->to_html(PORTFOLIO_ADD_MOODLE_URL);
return $url ?: null;
}
/**
* Get the url to view an author's profile.
*
* @param author_entity $author The author
* @param int $courseid The course id
* @return moodle_url
*/
public function get_author_profile_url(author_entity $author, int $courseid): moodle_url {
return new moodle_url('/user/view.php', [
'id' => $author->get_id(),
'course' => $courseid
]);
}
/**
* Get the url to view the author's profile image. The author's context id should be
* provided to prevent the code from needing to load it.
*
* @param author_entity $author The author
* @param int|null $authorcontextid The author context id
* @param int $size The size of the image to return
* @return moodle_url
*/
public function get_author_profile_image_url(
author_entity $author,
int $authorcontextid = null,
int $size = 100
): moodle_url {
global $PAGE;
$datamapper = $this->legacydatamapperfactory->get_author_data_mapper();
$record = $datamapper->to_legacy_object($author);
$record->contextid = $authorcontextid;
$userpicture = new user_picture($record);
$userpicture->size = $size;
return $userpicture->get_url($PAGE);
}
/**
* Get the url to view an author's group.
*
* @param \stdClass $group The group
* @return moodle_url
*/
public function get_author_group_url(\stdClass $group): moodle_url {
return new moodle_url('/user/index.php', [
'id' => $group->courseid,
'group' => $group->id
]);
}
/**
* Get the url to mark a discussion as read.
*
* @param forum_entity $forum The forum that the discussion belongs to
* @param discussion_entity $discussion The discussion
* @return moodle_url
*/
public function get_mark_discussion_as_read_url_from_discussion(
forum_entity $forum,
discussion_entity $discussion
): moodle_url {
return new moodle_url('/mod/forum/markposts.php', [
'f' => $discussion->get_forum_id(),
'd' => $discussion->get_id(),
'mark' => 'read',
'sesskey' => sesskey(),
'return' => $this->get_forum_view_url_from_forum($forum)->out(),
]);
}
/**
* Get the url to mark all discussions as read.
*
* @param forum_entity $forum The forum that the discussions belong to
* @return moodle_url
*/
public function get_mark_all_discussions_as_read_url(forum_entity $forum): moodle_url {
return new moodle_url('/mod/forum/markposts.php', [
'f' => $forum->get_id(),
'mark' => 'read',
'sesskey' => sesskey(),
'return' => $this->get_forum_view_url_from_forum($forum)->out(),
]);
}
/**
* Get the url to subscribe to a discussion.
*
* @param discussion_entity $discussion The discussion
* @return moodle_url
*/
public function get_discussion_subscribe_url(discussion_entity $discussion): moodle_url {
return new moodle_url('/mod/forum/subscribe.php', [
'sesskey' => sesskey(),
'id' => $discussion->get_forum_id(),
'd' => $discussion->get_id()
]);
}
/**
* Generate the pinned discussion link
*
* @param discussion_entity $discussion
* @return moodle_url
* @throws \moodle_exception
*/
public function get_pin_discussion_url_from_discussion(discussion_entity $discussion): moodle_url {
return new moodle_url('discuss.php', [
'sesskey' => sesskey(),
'd' => $discussion->get_id(),
'pin' => $discussion->is_pinned() ? FORUM_DISCUSSION_UNPINNED : FORUM_DISCUSSION_PINNED
]);
}
}
+163
View File
@@ -0,0 +1,163 @@
<?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 factory.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\factories;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\factories\entity as entity_factory;
use mod_forum\local\vaults\author as author_vault;
use mod_forum\local\vaults\discussion as discussion_vault;
use mod_forum\local\vaults\discussion_list as discussion_list_vault;
use mod_forum\local\vaults\forum as forum_vault;
use mod_forum\local\vaults\post as post_vault;
use mod_forum\local\vaults\post_attachment as post_attachment_vault;
use mod_forum\local\vaults\post_read_receipt_collection as post_read_receipt_collection_vault;
use file_storage;
use moodle_database;
/**
* Vault factory.
*
* See:
* https://designpatternsphp.readthedocs.io/en/latest/Creational/SimpleFactory/README.html
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class vault {
/** @var entity_factory $entityfactory Entity factory */
private $entityfactory;
/** @var legacy_data_mapper $legacymapper Entity factory */
private $legacymapper;
/** @var moodle_database $db A moodle database */
private $db;
/** @var file_storage $filestorage A file storage instance */
private $filestorage;
/**
* Constructor.
*
* @param moodle_database $db A moodle database
* @param entity_factory $entityfactory Entity factory
* @param file_storage $filestorage A file storage instance
* @param legacy_data_mapper $legacyfactory Datamapper
*/
public function __construct(moodle_database $db, entity_factory $entityfactory,
file_storage $filestorage, legacy_data_mapper $legacyfactory) {
$this->db = $db;
$this->entityfactory = $entityfactory;
$this->filestorage = $filestorage;
$this->legacymapper = $legacyfactory;
}
/**
* Create a forum vault.
*
* @return forum_vault
*/
public function get_forum_vault(): forum_vault {
return new forum_vault(
$this->db,
$this->entityfactory,
$this->legacymapper->get_legacy_data_mapper_for_vault('forum')
);
}
/**
* Create a discussion vault.
*
* @return discussion_vault
*/
public function get_discussion_vault(): discussion_vault {
return new discussion_vault(
$this->db,
$this->entityfactory,
$this->legacymapper->get_legacy_data_mapper_for_vault('discussion')
);
}
/**
* Create a discussion list vault.
*
* @return discussion_list_vault
*/
public function get_discussions_in_forum_vault(): discussion_list_vault {
return new discussion_list_vault(
$this->db,
$this->entityfactory,
$this->legacymapper->get_legacy_data_mapper_for_vault('discussion')
);
}
/**
* Create a post vault.
*
* @return post_vault
*/
public function get_post_vault(): post_vault {
return new post_vault(
$this->db,
$this->entityfactory,
$this->legacymapper->get_legacy_data_mapper_for_vault('post')
);
}
/**
* Create an author vault.
*
* @return author_vault
*/
public function get_author_vault(): author_vault {
return new author_vault(
$this->db,
$this->entityfactory,
$this->legacymapper->get_legacy_data_mapper_for_vault('author')
);
}
/**
* Create a post read receipt collection vault.
*
* @return post_read_receipt_collection_vault
*/
public function get_post_read_receipt_collection_vault(): post_read_receipt_collection_vault {
return new post_read_receipt_collection_vault(
$this->db,
$this->entityfactory,
$this->legacymapper->get_legacy_data_mapper_for_vault('post')
);
}
/**
* Create a post attachment vault.
*
* @return post_attachment_vault
*/
public function get_post_attachment_vault(): post_attachment_vault {
return new post_attachment_vault(
$this->filestorage
);
}
}
@@ -0,0 +1,732 @@
<?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/>.
/**
* Capability manager for the forum.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\managers;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\data_mappers\legacy\forum as legacy_forum_data_mapper;
use mod_forum\local\data_mappers\legacy\discussion as legacy_discussion_data_mapper;
use mod_forum\local\data_mappers\legacy\post as legacy_post_data_mapper;
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\subscriptions;
use context;
use context_system;
use stdClass;
use moodle_exception;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* Capability manager for the forum.
*
* Defines all the business rules for what a user can and can't do in the forum.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class capability {
/** @var legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper */
private $forumdatamapper;
/** @var legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper */
private $discussiondatamapper;
/** @var legacy_post_data_mapper $postdatamapper Legacy post data mapper */
private $postdatamapper;
/** @var forum_entity $forum Forum entity */
private $forum;
/** @var stdClass $forumrecord Legacy forum record */
private $forumrecord;
/** @var context $context Module context for the forum */
private $context;
/** @var array $canviewpostcache Cache of discussion posts that can be viewed by a user. */
protected $canviewpostcache = [];
/**
* Constructor.
*
* @param forum_entity $forum The forum entity to manage capabilities for.
* @param legacy_forum_data_mapper $forumdatamapper Legacy forum data mapper
* @param legacy_discussion_data_mapper $discussiondatamapper Legacy discussion data mapper
* @param legacy_post_data_mapper $postdatamapper Legacy post data mapper
*/
public function __construct(
forum_entity $forum,
legacy_forum_data_mapper $forumdatamapper,
legacy_discussion_data_mapper $discussiondatamapper,
legacy_post_data_mapper $postdatamapper
) {
$this->forumdatamapper = $forumdatamapper;
$this->discussiondatamapper = $discussiondatamapper;
$this->postdatamapper = $postdatamapper;
$this->forum = $forum;
$this->forumrecord = $forumdatamapper->to_legacy_object($forum);
$this->context = $forum->get_context();
}
/**
* Can the user subscribe to this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_subscribe_to_forum(stdClass $user): bool {
if ($this->forum->get_type() == 'single') {
return false;
}
return !is_guest($this->get_context(), $user) &&
subscriptions::is_subscribable($this->get_forum_record());
}
/**
* Can the user create discussions in this forum?
*
* @param stdClass $user The user to check
* @param int|null $groupid The current activity group id
* @return bool
*/
public function can_create_discussions(stdClass $user, int $groupid = null): bool {
if (isguestuser($user) or !isloggedin()) {
return false;
}
if ($this->forum->is_cutoff_date_reached()) {
if (!has_capability('mod/forum:canoverridecutoff', $this->get_context())) {
return false;
}
}
// If the user reaches the number of posts equal to warning/blocking setting then return the value of canpost in $warningobj.
$cmrecord = $this->forum->get_course_module_record();
if ($warningobj = forum_check_throttling($this->forumrecord, $cmrecord)) {
return $warningobj->canpost;
}
switch ($this->forum->get_type()) {
case 'news':
$capability = 'mod/forum:addnews';
break;
case 'qanda':
$capability = 'mod/forum:addquestion';
break;
default:
$capability = 'mod/forum:startdiscussion';
}
if (!has_capability($capability, $this->forum->get_context(), $user)) {
return false;
}
if ($this->forum->get_type() == 'eachuser') {
if (forum_user_has_posted_discussion($this->forum->get_id(), $user->id, $groupid)) {
return false;
}
}
if ($this->forum->is_in_group_mode()) {
return $groupid ? $this->can_access_group($user, $groupid) : $this->can_access_all_groups($user);
} else {
return true;
}
}
/**
* Can the user access all groups?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_access_all_groups(stdClass $user): bool {
return has_capability('moodle/site:accessallgroups', $this->get_context(), $user);
}
/**
* Can the user access the given group?
*
* @param stdClass $user The user to check
* @param int $groupid The id of the group that the forum is set to
* @return bool
*/
public function can_access_group(stdClass $user, int $groupid): bool {
if ($this->can_access_all_groups($user)) {
// This user has access to all groups.
return true;
}
// This is a group discussion for a forum in separate groups mode.
// Check if the user is a member.
// This is the most expensive check.
return groups_is_member($groupid, $user->id);
}
/**
* Can the user post to their groups?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_post_to_my_groups(stdClass $user): bool {
return has_capability('mod/forum:canposttomygroups', $this->get_context(), $user);
}
/**
* Can the user view discussions in this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_view_discussions(stdClass $user): bool {
return has_capability('mod/forum:viewdiscussion', $this->get_context(), $user);
}
/**
* Can the user move discussions in this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_move_discussions(stdClass $user): bool {
$forum = $this->get_forum();
return $forum->get_type() !== 'single' &&
has_capability('mod/forum:movediscussions', $this->get_context(), $user);
}
/**
* Can the user pin discussions in this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_pin_discussions(stdClass $user): bool {
return $this->forum->get_type() !== 'single' &&
has_capability('mod/forum:pindiscussions', $this->get_context(), $user);
}
/**
* Can the user split discussions in this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_split_discussions(stdClass $user): bool {
$forum = $this->get_forum();
return $forum->get_type() !== 'single' && has_capability('mod/forum:splitdiscussions', $this->get_context(), $user);
}
/**
* Can the user export (see portfolios) discussions in this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_export_discussions(stdClass $user): bool {
global $CFG;
return $CFG->enableportfolios && has_capability('mod/forum:exportdiscussion', $this->get_context(), $user);
}
/**
* Can the user manually mark posts as read/unread in this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_manually_control_post_read_status(stdClass $user): bool {
global $CFG;
return $CFG->forum_usermarksread && isloggedin() && forum_tp_is_tracked($this->get_forum_record(), $user);
}
/**
* Is the user required to post in the discussion before they can view it?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function must_post_before_viewing_discussion(stdClass $user, discussion_entity $discussion): bool {
$forum = $this->get_forum();
if ($forum->get_type() === 'qanda') {
// If it's a Q and A forum then the user must either have the capability to view without
// posting or the user must have posted before they can view the discussion.
return !has_capability('mod/forum:viewqandawithoutposting', $this->get_context(), $user) &&
!forum_user_has_posted($forum->get_id(), $discussion->get_id(), $user->id);
} else {
// No other forum types require posting before viewing.
return false;
}
}
/**
* Can the user subscribe to the give discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function can_subscribe_to_discussion(stdClass $user, discussion_entity $discussion): bool {
return $this->can_subscribe_to_forum($user);
}
/**
* Can the user move the discussion in this forum?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function can_move_discussion(stdClass $user, discussion_entity $discussion): bool {
return $this->can_move_discussions($user);
}
/**
* Is the user pin the discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function can_pin_discussion(stdClass $user, discussion_entity $discussion): bool {
return $this->can_pin_discussions($user);
}
/**
* Can the user post in this discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function can_post_in_discussion(stdClass $user, discussion_entity $discussion): bool {
$forum = $this->get_forum();
$forumrecord = $this->get_forum_record();
$discussionrecord = $this->get_discussion_record($discussion);
$context = $this->get_context();
$coursemodule = $forum->get_course_module_record();
$course = $forum->get_course_record();
$status = forum_user_can_post($forumrecord, $discussionrecord, $user, $coursemodule, $course, $context);
// If the user reaches the number of posts equal to warning/blocking setting then logically and canpost value with $status.
if ($warningobj = forum_check_throttling($forumrecord, $coursemodule)) {
return $status && $warningobj->canpost;
}
return $status;
}
/**
* Can the user favourite the discussion
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_favourite_discussion(stdClass $user): bool {
$context = $this->get_context();
return has_capability('mod/forum:cantogglefavourite', $context, $user);
}
/**
* Can the user view the content of a discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function can_view_discussion(stdClass $user, discussion_entity $discussion): bool {
$forumrecord = $this->get_forum_record();
$discussionrecord = $this->get_discussion_record($discussion);
$context = $this->get_context();
return forum_user_can_see_discussion($forumrecord, $discussionrecord, $context, $user);
}
/**
* Can the user view the content of the post in this discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @param post_entity $post The post the user wants to view
* @return bool
*/
public function can_view_post(stdClass $user, discussion_entity $discussion, post_entity $post): bool {
if (!$this->can_view_post_shell($user, $post)) {
return false;
}
// Return cached can view if possible.
if (isset($this->canviewpostcache[$user->id][$post->get_id()])) {
return $this->canviewpostcache[$user->id][$post->get_id()];
}
// Otherwise, check if the user can see this post.
$forum = $this->get_forum();
$forumrecord = $this->get_forum_record();
$discussionrecord = $this->get_discussion_record($discussion);
$postrecord = $this->get_post_record($post);
$coursemodule = $forum->get_course_module_record();
$canviewpost = forum_user_can_see_post($forumrecord, $discussionrecord, $postrecord, $user, $coursemodule, false);
// Then cache the result before returning.
$this->canviewpostcache[$user->id][$post->get_id()] = $canviewpost;
return $canviewpost;
}
/**
* Can the user view the post at all?
* In some situations the user can view the shell of a post without being able to view its content.
*
* @param stdClass $user The user to check
* @param post_entity $post The post the user wants to view
* @return bool
*
*/
public function can_view_post_shell(stdClass $user, post_entity $post): bool {
if ($post->is_owned_by_user($user)) {
return true;
}
if (!$post->is_private_reply()) {
return true;
}
if ($post->is_private_reply_intended_for_user($user)) {
return true;
}
return $this->can_view_any_private_reply($user);
}
/**
* Whether the user can view any private reply in the forum.
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_view_any_private_reply(stdClass $user): bool {
return has_capability('mod/forum:readprivatereplies', $this->get_context(), $user);
}
/**
* Can the user edit the post in this discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @param post_entity $post The post the user wants to edit
* @return bool
*/
public function can_edit_post(stdClass $user, discussion_entity $discussion, post_entity $post): bool {
global $CFG;
$context = $this->get_context();
$ownpost = $post->is_owned_by_user($user);
$ineditingtime = $post->get_age() < $CFG->maxeditingtime;
$mailnow = $post->should_mail_now();
switch ($this->forum->get_type()) {
case 'news':
// Allow editing of news posts once the discussion has started.
$ineditingtime = !$post->has_parent() && $discussion->has_started();
break;
case 'single':
if ($discussion->is_first_post($post)) {
return has_capability('moodle/course:manageactivities', $context, $user);
}
break;
}
return ($ownpost && $ineditingtime && !$mailnow) || has_capability('mod/forum:editanypost', $context, $user);
}
/**
* Verifies is the given user can delete a post.
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @param post_entity $post The post the user wants to delete
* @param bool $hasreplies Whether the post has replies
* @return bool
* @throws moodle_exception
*/
public function validate_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post,
bool $hasreplies = false): void {
global $CFG;
$forum = $this->get_forum();
if ($forum->get_type() == 'single' && $discussion->is_first_post($post)) {
// Do not allow deleting of first post in single simple type.
throw new moodle_exception('cannotdeletepost', 'forum');
}
$context = $this->get_context();
$ownpost = $post->is_owned_by_user($user);
$ineditingtime = $post->get_age() < $CFG->maxeditingtime;
$mailnow = $post->should_mail_now();
if (!($ownpost && $ineditingtime && has_capability('mod/forum:deleteownpost', $context, $user) && !$mailnow ||
has_capability('mod/forum:deleteanypost', $context, $user))) {
throw new moodle_exception('cannotdeletepost', 'forum');
}
if ($post->get_total_score()) {
throw new moodle_exception('couldnotdeleteratings', 'rating');
}
if ($hasreplies && !has_capability('mod/forum:deleteanypost', $context, $user)) {
throw new moodle_exception('couldnotdeletereplies', 'forum');
}
}
/**
* Can the user delete the post in this discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @param post_entity $post The post the user wants to delete
* @param bool $hasreplies Whether the post has replies
* @return bool
*/
public function can_delete_post(stdClass $user, discussion_entity $discussion, post_entity $post,
bool $hasreplies = false): bool {
try {
$this->validate_delete_post($user, $discussion, $post, $hasreplies);
return true;
} catch (moodle_exception $e) {
return false;
}
}
/**
* Can the user split the post in this discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @param post_entity $post The post the user wants to split
* @return bool
*/
public function can_split_post(stdClass $user, discussion_entity $discussion, post_entity $post): bool {
if ($post->is_private_reply()) {
// It is not possible to create a private discussion.
return false;
}
return $this->can_split_discussions($user) && $post->has_parent();
}
/**
* Can the user reply to the post in this discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @param post_entity $post The post the user wants to reply to
* @return bool
*/
public function can_reply_to_post(stdClass $user, discussion_entity $discussion, post_entity $post): bool {
if ($post->is_private_reply()) {
// It is not possible to reply to a private reply.
return false;
} else if (!$this->can_view_post($user, $discussion, $post)) {
// If the user cannot view the post in the first place, the user should not be able to reply to the post.
return false;
}
return $this->can_post_in_discussion($user, $discussion);
}
/**
* Can the user reply privately to the specified post?
*
* @param stdClass $user The user to check
* @param post_entity $post The post the user wants to reply to
* @return bool
*/
public function can_reply_privately_to_post(stdClass $user, post_entity $post): bool {
if ($post->is_private_reply()) {
// You cannot reply privately to a post which is, itself, a private reply.
return false;
}
return has_capability('mod/forum:postprivatereply', $this->get_context(), $user);
}
/**
* Can the user export (see portfolios) the post in this discussion?
*
* @param stdClass $user The user to check
* @param post_entity $post The post the user wants to export
* @return bool
*/
public function can_export_post(stdClass $user, post_entity $post): bool {
global $CFG;
$context = $this->get_context();
return $CFG->enableportfolios && (has_capability('mod/forum:exportpost', $context, $user) ||
($post->is_owned_by_user($user) && has_capability('mod/forum:exportownpost', $context, $user)));
}
/**
* Get the forum entity for this capability manager.
*
* @return forum_entity
*/
protected function get_forum(): forum_entity {
return $this->forum;
}
/**
* Get the legacy forum record for this forum.
*
* @return stdClass
*/
protected function get_forum_record(): stdClass {
return $this->forumrecord;
}
/**
* Get the context for this capability manager.
*
* @return context
*/
protected function get_context(): context {
return $this->context;
}
/**
* Get the legacy discussion record for the given discussion entity.
*
* @param discussion_entity $discussion The discussion to convert
* @return stdClass
*/
protected function get_discussion_record(discussion_entity $discussion): stdClass {
return $this->discussiondatamapper->to_legacy_object($discussion);
}
/**
* Get the legacy post record for the given post entity.
*
* @param post_entity $post The post to convert
* @return stdClass
*/
protected function get_post_record(post_entity $post): stdClass {
return $this->postdatamapper->to_legacy_object($post);
}
/**
* Can the user view the participants of this discussion?
*
* @param stdClass $user The user to check
* @param discussion_entity $discussion The discussion to check
* @return bool
*/
public function can_view_participants(stdClass $user, discussion_entity $discussion): bool {
return course_can_view_participants($this->get_context()) &&
!$this->must_post_before_viewing_discussion($user, $discussion);
}
/**
* Can the user view hidden posts in this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_view_hidden_posts(stdClass $user): bool {
return has_capability('mod/forum:viewhiddentimedposts', $this->get_context(), $user);
}
/**
* Can the user manage this forum?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_manage_forum(stdClass $user) {
return has_capability('moodle/course:manageactivities', $this->get_context(), $user);
}
/**
* Can the user manage tags on the site?
*
* @param stdClass $user The user to check
* @return bool
*/
public function can_manage_tags(stdClass $user): bool {
return has_capability('moodle/tag:manage', context_system::instance(), $user);
}
/**
* Checks whether the user can self enrol into the course.
* Mimics the checks on the add button in deprecatedlib/forum_print_latest_discussions
*
* @param stdClass $user
* @return bool
*/
public function can_self_enrol(stdClass $user): bool {
$canstart = false;
if ($this->forum->get_type() != 'news') {
if (isguestuser($user) or !isloggedin()) {
$canstart = true;
}
if (!is_enrolled($this->context) and !is_viewing($this->context)) {
// Allow guests and not-logged-in to see the button - they are prompted to log in after clicking the link,
// Normal users with temporary guest access see this button too, they are asked to enrol instead,
// Do not show the button to users with suspended enrolments here.
$canstart = enrol_selfenrol_available($this->forum->get_course_id());
}
}
return $canstart;
}
/**
* Checks whether the user can export the whole forum (discussions and posts).
*
* @param stdClass $user The user object.
* @return bool True if the user can export the forum or false otherwise.
*/
public function can_export_forum(stdClass $user): bool {
return has_capability('mod/forum:exportforum', $this->get_context(), $user);
}
/**
* Check whether the supplied grader can grade the gradee.
*
* @param stdClass $grader The user grading
* @param stdClass $gradee The user being graded
* @return bool
*/
public function can_grade(stdClass $grader, stdClass $gradee = null): bool {
if (!has_capability('mod/forum:grade', $this->get_context(), $grader)) {
return false;
}
return true;
}
}
@@ -0,0 +1,461 @@
<?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/>.
/**
* Discussion renderer.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\renderers;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\discussion as discussion_entity;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\entities\sorter as sorter_entity;
use mod_forum\local\factories\entity as entity_factory;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\url as url_factory;
use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\managers\capability as capability_manager;
use mod_forum\local\renderers\posts as posts_renderer;
use forum_portfolio_caller;
use core\output\notification;
use context;
use context_module;
use html_writer;
use moodle_exception;
use moodle_page;
use moodle_url;
use rating_manager;
use renderer_base;
use single_button;
use single_select;
use stdClass;
use url_select;
require_once($CFG->dirroot . '/mod/forum/lib.php');
require_once($CFG->dirroot . '/mod/forum/locallib.php');
/**
* Discussion renderer class.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion {
/** @var forum_entity $forum The forum that the discussion belongs to */
private $forum;
/** @var discussion_entity $discussion The discussion entity */
private $discussion;
/** @var stdClass $discussionrecord Legacy discussion record */
private $discussionrecord;
/** @var stdClass $forumrecord Legacy forum record */
private $forumrecord;
/** @var int $displaymode The display mode to render the discussion in */
private $displaymode;
/** @var renderer_base $renderer Renderer base */
private $renderer;
/** @var posts_renderer $postsrenderer A posts renderer */
private $postsrenderer;
/** @var moodle_page $page The page this discussion is being rendered for */
private $page;
/** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */
private $legacydatamapperfactory;
/** @var exporter_factory $exporterfactory Exporter factory */
private $exporterfactory;
/** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory;
/** @var url_factory $urlfactory URL factory */
private $urlfactory;
/** @var entity_factory $entityfactory Entity factory */
private $entityfactory;
/** @var capability_manager $capabilitymanager Capability manager */
private $capabilitymanager;
/** @var rating_manager $ratingmanager Rating manager */
private $ratingmanager;
/** @var moodle_url $baseurl The base URL for the discussion */
private $baseurl;
/** @var array $notifications List of HTML notifications to display */
private $notifications;
/** @var sorter_entity $exportedpostsorter Sorter for the exported posts */
private $exportedpostsorter;
/** @var callable $postprocessfortemplate Function to process exported posts before template rendering */
private $postprocessfortemplate;
/**
* Constructor.
*
* @param forum_entity $forum The forum that the discussion belongs to
* @param discussion_entity $discussion The discussion entity
* @param int $displaymode The display mode to render the discussion in
* @param renderer_base $renderer Renderer base
* @param posts_renderer $postsrenderer A posts renderer
* @param moodle_page $page The page this discussion is being rendered for
* @param legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory
* @param exporter_factory $exporterfactory Exporter factory
* @param vault_factory $vaultfactory Vault factory
* @param url_factory $urlfactory URL factory
* @param entity_factory $entityfactory Entity factory
* @param capability_manager $capabilitymanager Capability manager
* @param rating_manager $ratingmanager Rating manager
* @param sorter_entity $exportedpostsorter Sorter for the exported posts
* @param moodle_url $baseurl The base URL for the discussion
* @param array $notifications List of HTML notifications to display
* @param callable|null $postprocessfortemplate Post processing for template callback
*/
public function __construct(
forum_entity $forum,
discussion_entity $discussion,
int $displaymode,
renderer_base $renderer,
posts_renderer $postsrenderer,
moodle_page $page,
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
url_factory $urlfactory,
entity_factory $entityfactory,
capability_manager $capabilitymanager,
rating_manager $ratingmanager,
sorter_entity $exportedpostsorter,
moodle_url $baseurl,
array $notifications = [],
callable $postprocessfortemplate = null
) {
$this->forum = $forum;
$this->discussion = $discussion;
$this->displaymode = $displaymode;
$this->renderer = $renderer;
$this->postsrenderer = $postsrenderer;
$this->page = $page;
$this->baseurl = $baseurl;
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->urlfactory = $urlfactory;
$this->entityfactory = $entityfactory;
$this->capabilitymanager = $capabilitymanager;
$this->ratingmanager = $ratingmanager;
$this->notifications = $notifications;
$this->exportedpostsorter = $exportedpostsorter;
$this->postprocessfortemplate = $postprocessfortemplate;
$forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper();
$this->forumrecord = $forumdatamapper->to_legacy_object($forum);
$discussiondatamapper = $this->legacydatamapperfactory->get_discussion_data_mapper();
$this->discussionrecord = $discussiondatamapper->to_legacy_object($discussion);
}
/**
* Render the discussion for the given user in the specified display mode.
*
* @param stdClass $user The user viewing the discussion
* @param post_entity $firstpost The first post in the discussion
* @param array $replies List of replies to the first post
* @return string HTML for the discussion
*/
public function render(
stdClass $user,
post_entity $firstpost,
array $replies
): string {
global $CFG;
$displaymode = $this->displaymode;
$capabilitymanager = $this->capabilitymanager;
$urlfactory = $this->urlfactory;
$entityfactory = $this->entityfactory;
// Make sure we can render.
if (!$capabilitymanager->can_view_discussions($user)) {
throw new moodle_exception('noviewdiscussionspermission', 'mod_forum');
}
$posts = array_merge([$firstpost], array_values($replies));
if ($this->postprocessfortemplate !== null) {
$exporteddiscussion = ($this->postprocessfortemplate) ($this->discussion, $user, $this->forum);
} else {
$exporteddiscussion = $this->get_exported_discussion($user);
}
$hasanyactions = false;
$hasanyactions = $hasanyactions || $capabilitymanager->can_favourite_discussion($user);
$hasanyactions = $hasanyactions || $capabilitymanager->can_pin_discussions($user);
$hasanyactions = $hasanyactions || $capabilitymanager->can_manage_forum($user);
$exporteddiscussion = array_merge($exporteddiscussion, [
'notifications' => $this->get_notifications($user),
'html' => [
'hasanyactions' => $hasanyactions,
'posts' => $this->postsrenderer->render($user, [$this->forum], [$this->discussion], $posts),
'modeselectorform' => $this->get_display_mode_selector_html($displaymode, $user),
'subscribe' => null,
'movediscussion' => null,
'pindiscussion' => null,
'neighbourlinks' => $this->get_neighbour_links_html(),
'exportdiscussion' => !empty($CFG->enableportfolios) ? $this->get_export_discussion_html($user) : null
],
'settingsselector' => true,
]);
$capabilities = (array) $exporteddiscussion['capabilities'];
if ($capabilities['move']) {
$exporteddiscussion['html']['movediscussion'] = $this->get_move_discussion_html();
}
if (!empty($user->id)) {
$loggedinuser = $entityfactory->get_author_from_stdClass($user);
$exporteddiscussion['loggedinuser'] = [
'firstname' => $loggedinuser->get_first_name(),
'fullname' => $loggedinuser->get_full_name(),
'profileimageurl' => ($urlfactory->get_author_profile_image_url($loggedinuser, null))->out(false)
];
}
$exporteddiscussion['throttlingwarningmsg'] = '';
$cmrecord = $this->forum->get_course_module_record();
if (($warningobj = forum_check_throttling($this->forumrecord, $cmrecord)) && $warningobj->canpost) {
$throttlewarnnotification = (new notification(
get_string($warningobj->errorcode, $warningobj->module, $warningobj->additional)
))->set_show_closebutton();
$exporteddiscussion['throttlingwarningmsg'] = $throttlewarnnotification->get_message();
}
if ($this->displaymode === FORUM_MODE_NESTED_V2) {
$template = 'mod_forum/forum_discussion_nested_v2';
} else {
$template = 'mod_forum/forum_discussion';
}
return $this->renderer->render_from_template($template, $exporteddiscussion);
}
/**
* Get the groups details for all groups available to the forum.
*
* @return stdClass[]
*/
private function get_groups_available_in_forum(): array {
$course = $this->forum->get_course_record();
$coursemodule = $this->forum->get_course_module_record();
return groups_get_all_groups($course->id, 0, $coursemodule->groupingid);
}
/**
* Get the exported discussion.
*
* @param stdClass $user The user viewing the discussion
* @return array
*/
private function get_exported_discussion(stdClass $user): array {
$discussionexporter = $this->exporterfactory->get_discussion_exporter(
$user,
$this->forum,
$this->discussion,
$this->get_groups_available_in_forum()
);
return (array) $discussionexporter->export($this->renderer);
}
/**
* Get the HTML for the display mode selector.
*
* @param int $displaymode The current display mode
* @param stdClass $user The current user
* @return string
*/
private function get_display_mode_selector_html(int $displaymode, stdClass $user): string {
$baseurl = $this->baseurl;
$select = new single_select(
$baseurl,
'mode',
forum_get_layout_modes(get_user_preferences('forum_useexperimentalui', false, $user)),
$displaymode,
null,
'mode'
);
$select->set_label(get_string('displaymode', 'forum'), ['class' => 'accesshide']);
return $this->renderer->render($select);
}
/**
* Get the HTML to render the move discussion selector and button.
*
* @return string
*/
private function get_move_discussion_html(): ?string {
global $DB;
$forum = $this->forum;
$discussion = $this->discussion;
$courseid = $forum->get_course_id();
// Popup menu to move discussions to other forums. The discussion in a
// single discussion forum can't be moved.
$modinfo = get_fast_modinfo($courseid);
if (isset($modinfo->instances['forum'])) {
$forummenu = [];
// Check forum types and eliminate simple discussions.
$forumcheck = $DB->get_records('forum', ['course' => $courseid], '', 'id, type');
foreach ($modinfo->instances['forum'] as $forumcm) {
if (!$forumcm->uservisible || !has_capability('mod/forum:startdiscussion',
context_module::instance($forumcm->id))) {
continue;
}
$section = $forumcm->sectionnum;
$sectionname = get_section_name($courseid, $section);
if (empty($forummenu[$section])) {
$forummenu[$section] = [$sectionname => []];
}
$forumidcompare = $forumcm->instance != $forum->get_id();
$forumtypecheck = $forumcheck[$forumcm->instance]->type !== 'single';
if ($forumidcompare and $forumtypecheck) {
$url = "/mod/forum/discuss.php?d={$discussion->get_id()}&move=$forumcm->instance&sesskey=".sesskey();
$forummenu[$section][$sectionname][$url] = format_string($forumcm->name);
}
}
if (!empty($forummenu)) {
$html = '<div class="movediscussionoption">';
$movebutton = get_string('move');
if ($this->displaymode === FORUM_MODE_NESTED_V2) {
// Move discussion selector will be rendered on the settings drawer. We won't output the button in this mode.
$movebutton = null;
}
$select = new url_select($forummenu, '',
['/mod/forum/discuss.php?d=' . $discussion->get_id() => get_string("movethisdiscussionto", "forum")],
'forummenu', $movebutton);
$select->set_label(get_string('movethisdiscussionlabel', 'mod_forum'), [
'class' => 'sr-only',
]);
$html .= $this->renderer->render($select);
$html .= "</div>";
return $html;
}
}
return null;
}
/**
* Get the HTML to render the export discussion button.
*
* @param stdClass $user The user viewing the discussion
* @return string|null
*/
private function get_export_discussion_html(stdClass $user): ?string {
global $CFG;
if (!$this->capabilitymanager->can_export_discussions($user)) {
return null;
}
$button = new \portfolio_add_button();
$button->set_callback_options('forum_portfolio_caller', ['discussionid' => $this->discussion->get_id()], 'mod_forum');
$button = $button->to_html(PORTFOLIO_ADD_FULL_FORM, get_string('exportdiscussion', 'mod_forum'));
return $button ?: null;
}
/**
* Get a list of notification HTML to render in the page.
*
* @param stdClass $user The user viewing the discussion
* @return string[]
*/
private function get_notifications($user): array {
$notifications = $this->notifications;
$discussion = $this->discussion;
$forum = $this->forum;
if ($forum->is_cutoff_date_reached()) {
$notifications[] = (new notification(
get_string('cutoffdatereached', 'forum'),
notification::NOTIFY_INFO
))->set_show_closebutton();
} else if ($forum->get_type() != 'single') {
// Due date is already shown at the top of the page for single simple discussion forums.
if ($forum->is_due_date_reached()) {
$notifications[] = (new notification(
get_string('thisforumisdue', 'forum', userdate($forum->get_due_date())),
notification::NOTIFY_INFO
))->set_show_closebutton();
} else if ($forum->has_due_date()) {
$notifications[] = (new notification(
get_string('thisforumhasduedate', 'forum', userdate($forum->get_due_date())),
notification::NOTIFY_INFO
))->set_show_closebutton();
}
}
if ($forum->is_discussion_locked($discussion)) {
$notifications[] = (new notification(
get_string('discussionlocked', 'forum'),
notification::NOTIFY_INFO
))
->set_extra_classes(['discussionlocked'])
->set_show_closebutton();
}
if ($forum->get_type() == 'qanda') {
if ($this->capabilitymanager->must_post_before_viewing_discussion($user, $discussion)) {
$notifications[] = (new notification(
get_string('qandanotify', 'forum')
))->set_show_closebutton(true)->set_extra_classes(['mt-3']);
}
}
if ($forum->has_blocking_enabled()) {
$notifications[] = (new notification(
get_string('thisforumisthrottled', 'forum', [
'blockafter' => $forum->get_block_after(),
'blockperiod' => get_string('secondstotime' . $forum->get_block_period())
]),
notification::NOTIFY_INFO
))->set_show_closebutton();
}
return array_map(function($notification) {
return $notification->export_for_template($this->renderer);
}, $notifications);
}
/**
* Get HTML to display the neighbour links.
*
* @return string
*/
private function get_neighbour_links_html(): string {
$forum = $this->forum;
$coursemodule = $forum->get_course_module_record();
$neighbours = forum_get_discussion_neighbours($coursemodule, $this->discussionrecord, $this->forumrecord);
return $this->renderer->neighbouring_discussion_navigation($neighbours['prev'], $neighbours['next']);
}
}
@@ -0,0 +1,425 @@
<?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/>.
/**
* Discussion list renderer.
*
* @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\renderers;
defined('MOODLE_INTERNAL') || die();
use mod_forum\grades\forum_gradeitem;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\factories\legacy_data_mapper as legacy_data_mapper_factory;
use mod_forum\local\factories\exporter as exporter_factory;
use mod_forum\local\factories\vault as vault_factory;
use mod_forum\local\factories\url as url_factory;
use mod_forum\local\managers\capability as capability_manager;
use mod_forum\local\vaults\discussion_list as discussion_list_vault;
use renderer_base;
use stdClass;
use core\output\notification;
use mod_forum\local\data_mappers\legacy\forum;
use mod_forum\local\factories\builder as builder_factory;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* The discussion list renderer.
*
* @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 {
/** @var forum_entity The forum being rendered */
private $forum;
/** @var stdClass The DB record for the forum being rendered */
private $forumrecord;
/** @var renderer_base The renderer used to render the view */
private $renderer;
/** @var legacy_data_mapper_factory $legacydatamapperfactory Legacy data mapper factory */
private $legacydatamapperfactory;
/** @var exporter_factory $exporterfactory Exporter factory */
private $exporterfactory;
/** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory;
/** @var capability_manager $capabilitymanager Capability manager */
private $capabilitymanager;
/** @var url_factory $urlfactory URL factory */
private $urlfactory;
/** @var array $notifications List of notification HTML */
private $notifications;
/** @var builder_factory $builderfactory Builder factory */
private $builderfactory;
/** @var callable $postprocessfortemplate Function to process exported posts before template rendering */
private $postprocessfortemplate;
/** @var string $template The template to use when displaying */
private $template;
/** @var gradeitem The gradeitem instance associated with this forum */
private $forumgradeitem;
/**
* Constructor for a new discussion list renderer.
*
* @param forum_entity $forum The forum entity to be rendered
* @param renderer_base $renderer The renderer used to render the view
* @param legacy_data_mapper_factory $legacydatamapperfactory The factory used to fetch a legacy record
* @param exporter_factory $exporterfactory The factory used to fetch exporter instances
* @param vault_factory $vaultfactory The factory used to fetch the vault instances
* @param builder_factory $builderfactory The factory used to fetch the builder instances
* @param capability_manager $capabilitymanager The managed used to check capabilities on the forum
* @param url_factory $urlfactory The factory used to create URLs in the forum
* @param string $template
* @param notification[] $notifications A list of any notifications to be displayed within the page
* @param callable|null $postprocessfortemplate Callback function to process discussion lists for templates
*/
public function __construct(
forum_entity $forum,
renderer_base $renderer,
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
builder_factory $builderfactory,
capability_manager $capabilitymanager,
url_factory $urlfactory,
forum_gradeitem $forumgradeitem,
string $template,
array $notifications = [],
callable $postprocessfortemplate = null
) {
$this->forum = $forum;
$this->renderer = $renderer;
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->builderfactory = $builderfactory;
$this->capabilitymanager = $capabilitymanager;
$this->urlfactory = $urlfactory;
$this->notifications = $notifications;
$this->postprocessfortemplate = $postprocessfortemplate;
$this->template = $template;
$this->forumgradeitem = $forumgradeitem;
$forumdatamapper = $this->legacydatamapperfactory->get_forum_data_mapper();
$this->forumrecord = $forumdatamapper->to_legacy_object($forum);
}
/**
* Render for the specified user.
*
* @param stdClass $user The user to render for
* @param cm_info $cm The course module info for this discussion list
* @param int $groupid The group to render
* @param int $sortorder The sort order to use when selecting the discussions in the list
* @param int $pageno The zero-indexed page number to use
* @param int $pagesize The number of discussions to show on the page
* @param int $displaymode The discussion display mode
* @param bool $enablediscussioncreation To show the discussion button.
* @return string The rendered content for display
*/
public function render(
stdClass $user,
\cm_info $cm,
?int $groupid,
?int $sortorder,
?int $pageno,
?int $pagesize,
int $displaymode = null,
bool $enablediscussioncreation = true
): string {
global $PAGE;
$forum = $this->forum;
$course = $forum->get_course_record();
$forumexporter = $this->exporterfactory->get_forum_exporter(
$user,
$this->forum,
$groupid
);
$pagesize = $this->get_page_size($pagesize);
$pageno = $this->get_page_number($pageno);
// Count all forum discussion posts.
$alldiscussionscount = mod_forum_count_all_discussions($forum, $user, $groupid);
// Get all forum discussion summaries.
$discussions = mod_forum_get_discussion_summaries($forum, $user, $groupid, $sortorder, $pageno, $pagesize);
$capabilitymanager = $this->capabilitymanager;
$hasanyactions = false;
$hasanyactions = $hasanyactions || $capabilitymanager->can_favourite_discussion($user);
$hasanyactions = $hasanyactions || $capabilitymanager->can_pin_discussions($user);
$hasanyactions = $hasanyactions || $capabilitymanager->can_manage_forum($user);
$forumview = [
'forum' => (array) $forumexporter->export($this->renderer),
'contextid' => $forum->get_context()->id,
'cmid' => $cm->id,
'groupid' => $groupid,
'name' => format_string($forum->get_name()),
'courseid' => $course->id,
'coursename' => format_string($course->shortname),
'experimentaldisplaymode' => $displaymode == FORUM_MODE_NESTED_V2,
'gradingcomponent' => $this->forumgradeitem->get_grading_component_name(),
'gradingcomponentsubtype' => $this->forumgradeitem->get_grading_component_subtype(),
'sendstudentnotifications' => $forum->should_notify_students_default_when_grade_for_forum(),
'gradeonlyactiveusers' => $this->forumgradeitem->should_grade_only_active_users(),
'hasanyactions' => $hasanyactions,
'groupchangemenu' => groups_print_activity_menu(
$cm,
$this->urlfactory->get_forum_view_url_from_forum($forum),
true
),
'hasmore' => ($alldiscussionscount > $pagesize),
'notifications' => $this->get_notifications($user, $groupid),
'settings' => [
'excludetext' => true,
'togglemoreicon' => true,
'excludesubscription' => true
],
'totaldiscussioncount' => $alldiscussionscount,
'userid' => $user->id,
'visiblediscussioncount' => count($discussions),
'enablediscussioncreation' => $enablediscussioncreation,
];
if ($forumview['forum']['capabilities']['create']) {
$forumview['newdiscussionhtml'] = $this->get_discussion_form($user, $cm, $groupid);
}
if (!$discussions) {
return $this->renderer->render_from_template($this->template, $forumview);
}
if ($this->postprocessfortemplate !== null) {
// We've got some post processing to do!
$exportedposts = ($this->postprocessfortemplate) ($discussions, $user, $forum);
}
$baseurl = new \moodle_url($PAGE->url, ['o' => $sortorder, 's' => $pagesize]);
$forumview = array_merge(
$forumview,
[
'pagination' => $this->renderer->render(new \paging_bar($alldiscussionscount, $pageno, $pagesize, $baseurl, 'p')),
],
$exportedposts
);
$firstdiscussion = reset($discussions);
$forumview['firstgradeduserid'] = $firstdiscussion->get_latest_post_author()->get_id();
return $this->renderer->render_from_template($this->template, $forumview);
}
/**
* Add new discussion button to the action bar for tertiary nav.
*
* @param stdClass $user The user object.
* @param int|null $groupid The group id.
* @return string rendered HTML string
*/
public function render_new_discussion(stdClass $user, ?int $groupid): string {
$forumexporter = $this->exporterfactory->get_forum_exporter(
$user,
$this->forum,
$groupid
);
$forumview = [
'forum' => (array) $forumexporter->export($this->renderer),
];
return $this->renderer->render_from_template('mod_forum/forum_new_discussion_actionbar', $forumview);
}
/**
* Get the mod_forum_post_form. This is the default boiler plate from mod_forum/post_form.php with the inpage flag caveat
*
* @param stdClass $user The user the form is being generated for
* @param \cm_info $cm
* @param int $groupid The groupid if any
*
* @return string The rendered html
*/
private function get_discussion_form(stdClass $user, \cm_info $cm, ?int $groupid) {
$forum = $this->forum;
$forumrecord = $this->legacydatamapperfactory->get_forum_data_mapper()->to_legacy_object($forum);
$modcontext = \context_module::instance($cm->id);
$coursecontext = \context_course::instance($forum->get_course_id());
$post = (object) [
'course' => $forum->get_course_id(),
'forum' => $forum->get_id(),
'discussion' => 0, // Ie discussion # not defined yet.
'parent' => 0,
'subject' => '',
'userid' => $user->id,
'message' => '',
'messageformat' => editors_get_preferred_format(),
'messagetrust' => 0,
'groupid' => $groupid,
];
$thresholdwarning = forum_check_throttling($forumrecord, $cm);
$formparams = array(
'course' => $forum->get_course_record(),
'cm' => $cm,
'coursecontext' => $coursecontext,
'modcontext' => $modcontext,
'forum' => $forumrecord,
'post' => $post,
'subscribe' => \mod_forum\subscriptions::is_subscribed($user->id, $forumrecord,
null, $cm),
'thresholdwarning' => $thresholdwarning,
'inpagereply' => true,
'edit' => 0
);
$posturl = new \moodle_url('/mod/forum/post.php');
$mformpost = new \mod_forum_post_form($posturl, $formparams, 'post', '', array('id' => 'mformforum'));
$discussionsubscribe = \mod_forum\subscriptions::get_user_default_subscription($forumrecord, $coursecontext, $cm, null);
$params = array('reply' => 0, 'forum' => $forumrecord->id, 'edit' => 0) +
(isset($post->groupid) ? array('groupid' => $post->groupid) : array()) +
array(
'userid' => $post->userid,
'parent' => $post->parent,
'discussion' => $post->discussion,
'course' => $forum->get_course_id(),
'discussionsubscribe' => $discussionsubscribe
);
$mformpost->set_data($params);
return $mformpost->render();
}
/**
* Fetch the page size to use when displaying the page.
*
* @param int $pagesize The number of discussions to show on the page
* @return int The normalised page size
*/
private function get_page_size(?int $pagesize): int {
if (null === $pagesize || $pagesize <= 0) {
$pagesize = discussion_list_vault::PAGESIZE_DEFAULT;
}
return $pagesize;
}
/**
* Fetch the current page number (zero-indexed).
*
* @param int $pageno The zero-indexed page number to use
* @return int The normalised page number
*/
private function get_page_number(?int $pageno): int {
if (null === $pageno || $pageno < 0) {
$pageno = 0;
}
return $pageno;
}
/**
* Get the list of notification for display.
*
* @param stdClass $user The viewing user
* @param int|null $groupid The forum's group id
* @return array
*/
private function get_notifications(stdClass $user, ?int $groupid): array {
$notifications = $this->notifications;
$forum = $this->forum;
$capabilitymanager = $this->capabilitymanager;
if ($forum->is_cutoff_date_reached()) {
$notifications[] = (new notification(
get_string('cutoffdatereached', 'forum'),
notification::NOTIFY_INFO
))->set_show_closebutton();
}
if ($forum->has_blocking_enabled()) {
$notifications[] = (new notification(
get_string('thisforumisthrottled', 'forum', [
'blockafter' => $forum->get_block_after(),
'blockperiod' => get_string('secondstotime' . $forum->get_block_period())
]),
notification::NOTIFY_INFO
))->set_show_closebutton();
}
if ($forum->is_in_group_mode() && !$capabilitymanager->can_access_all_groups($user)) {
if ($groupid === null) {
if (!$capabilitymanager->can_post_to_my_groups($user)) {
$notifications[] = (new notification(
get_string('cannotadddiscussiongroup', 'mod_forum'),
\core\output\notification::NOTIFY_WARNING
))->set_show_closebutton();
} else {
$notifications[] = (new notification(
get_string('cannotadddiscussionall', 'mod_forum'),
\core\output\notification::NOTIFY_WARNING
))->set_show_closebutton();
}
} else if (!$capabilitymanager->can_access_group($user, $groupid)) {
$notifications[] = (new notification(
get_string('cannotadddiscussion', 'mod_forum'),
\core\output\notification::NOTIFY_WARNING
))->set_show_closebutton();
}
}
if ('qanda' === $forum->get_type() && !$capabilitymanager->can_manage_forum($user)) {
$notifications[] = (new notification(
get_string('qandanotify', 'forum'),
notification::NOTIFY_INFO
))->set_show_closebutton()->set_extra_classes(['mt-3']);
}
if ('eachuser' === $forum->get_type()) {
$notifications[] = (new notification(
get_string('allowsdiscussions', 'forum'),
notification::NOTIFY_INFO)
)->set_show_closebutton();
}
return array_map(function($notification) {
return $notification->export_for_template($this->renderer);
}, $notifications);
}
}
+111
View File
@@ -0,0 +1,111 @@
<?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/>.
/**
* Posts renderer.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\renderers;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\builders\exported_posts as exported_posts_builder;
use renderer_base;
use stdClass;
/**
* Posts renderer class.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class posts {
/** @var renderer_base $renderer Renderer base */
private $renderer;
/** @var exported_posts_builder $exportedpostsbuilder Builder for building exported posts */
private $exportedpostsbuilder;
/** @var string $template The template to render */
private $template;
/** @var callable $postprocessfortemplate Function to process exported posts before template rendering */
private $postprocessfortemplate;
/**
* Constructor.
*
* @param renderer_base $renderer Renderer base
* @param exported_posts_builder $exportedpostsbuilder Builder for building exported posts
* @param string $template The template to render
* @param callable $postprocessfortemplate Function to process exported posts before template rendering
*/
public function __construct(
renderer_base $renderer,
exported_posts_builder $exportedpostsbuilder,
string $template,
callable $postprocessfortemplate = null
) {
$this->renderer = $renderer;
$this->exportedpostsbuilder = $exportedpostsbuilder;
$this->template = $template;
$this->postprocessfortemplate = $postprocessfortemplate;
}
/**
* Render the given posts for the forums and discussions.
*
* @param stdClass $user The user viewing the posts
* @param forum_entity[] $forums A list of all forums for these posts
* @param discussion_entity[] $discussions A list of all discussions for these posts
* @param post_entity[] $posts The posts to render
* @return string
*/
public function render(
stdClass $user,
array $forums,
array $discussions,
array $posts
): string {
// Format the forums and discussion to make them more easily accessed later.
$forums = array_reduce($forums, function($carry, $forum) {
$carry[$forum->get_id()] = $forum;
return $carry;
}, []);
$discussions = array_reduce($discussions, function($carry, $discussion) {
$carry[$discussion->get_id()] = $discussion;
return $carry;
}, []);
$exportedposts = $this->exportedpostsbuilder->build(
$user,
$forums,
$discussions,
$posts
);
if ($this->postprocessfortemplate !== null) {
// We've got some post processing to do!
$exportedposts = ($this->postprocessfortemplate)($exportedposts, $forums, $discussions, $user);
}
return $this->renderer->render_from_template(
$this->template,
['posts' => array_values($exportedposts)]
);
}
}
+120
View File
@@ -0,0 +1,120 @@
<?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/>.
/**
* Author vault class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
/**
* Author vault class.
*
* 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
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class author extends db_table_vault {
/** The table for this vault */
private const TABLE = 'user';
/**
* Get the table alias.
*
* @return string
*/
protected function get_table_alias(): string {
return 'a';
}
/**
* 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 user ID
* @return string
*/
protected function generate_get_records_sql(string $wheresql = null, string $sortsql = null, ?int $userid = null): string {
$selectsql = 'SELECT * FROM {' . self::TABLE . '} ' . $this->get_table_alias();
$selectsql .= $wheresql ? ' WHERE ' . $wheresql : '';
$selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : '';
return $selectsql;
}
/**
* Convert the DB records into author entities.
*
* @param array $results The DB records
* @return author_entity[]
*/
protected function from_db_records(array $results) {
$entityfactory = $this->get_entity_factory();
return array_map(function(array $result) use ($entityfactory) {
[
'record' => $record,
] = $result;
return $entityfactory->get_author_from_stdclass($record);
}, $results);
}
/**
* Get the authors for the given posts.
*
* Returns a distinct list of authors indexed by author id.
*
* @param post_entity[] $posts The list of posts
* @return author_entity[]
*/
public function get_authors_for_posts(array $posts): array {
$authorids = array_reduce($posts, function($carry, $post) {
$carry[$post->get_author_id()] = true;
return $carry;
}, []);
$authorids = array_keys($authorids);
return $this->get_from_ids($authorids);
}
/**
* Get the context ids for a set of author ids. The results are indexed
* by the author id.
*
* @param int[] $authorids The list of author ids to fetch.
* @return int[] Results indexed by author id.
*/
public function get_context_ids_for_author_ids(array $authorids): array {
$db = $this->get_db();
[$insql, $params] = $db->get_in_or_equal($authorids);
$sql = "SELECT instanceid, id FROM {context} WHERE contextlevel = ? AND instanceid {$insql}";
$records = $db->get_records_sql($sql, array_merge([CONTEXT_USER], $params));
return array_reduce($authorids, function($carry, $id) use ($records) {
$carry[$id] = isset($records[$id]) ? (int) $records[$id]->id : null;
return $carry;
}, []);
}
}
@@ -0,0 +1,189 @@
<?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/>.
/**
* Abstract class for loading records from the DB.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\factories\entity as entity_factory;
use moodle_database;
/**
* Abstract class for loading records from the DB.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class db_table_vault {
/** @var moodle_database $db A moodle database */
private $db;
/** @var entity_factory $entityfactory Entity factory */
private $entityfactory;
/** @var object $legacyfactory Entity->legacy factory */
private $legacyfactory;
/**
* Constructor.
*
* @param moodle_database $db A moodle database
* @param entity_factory $entityfactory Entity factory
* @param object $legacyfactory Legacy factory
*/
public function __construct(
moodle_database $db,
entity_factory $entityfactory,
$legacyfactory
) {
$this->db = $db;
$this->entityfactory = $entityfactory;
$this->legacyfactory = $legacyfactory;
}
/**
* Get the table alias.
*
* @return string
*/
abstract protected function get_table_alias(): string;
/**
* 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 object|null $user The user object
* @return string
*/
abstract protected function generate_get_records_sql(string $wheresql = null, string $sortsql = null,
?int $userid = null): string;
/**
* Convert the DB records into entities. The list of records will have been
* passed through any preprocessors that may be defined before being given to
* this function.
*
* Each result will from the list will be an associative array where the key
* is set to the identifier given to the preprocessor and the result is the value
* generated by the preprocessor.
*
* All results will have a 'record' key who's value is the original DB record.
*
* @param array $results The DB records
*/
abstract protected function from_db_records(array $results);
/**
* Get the list of preprocessors to run on the DB record results. The preprocessors
* should be defined using an associative array. The key used to identify the
* preprocessor in this list will be used to identify the value of that preprocessor
* in the list of results when passed to the from_db_records function.
*
* @return array
*/
protected function get_preprocessors(): array {
return [];
}
/**
* Get the moodle database.
*
* @return moodle_database
*/
protected function get_db(): moodle_database {
return $this->db;
}
/**
* Get the entity factory.
*
* @return entity_factory
*/
protected function get_entity_factory(): entity_factory {
return $this->entityfactory;
}
/**
* Get the legacy factory
*
* @return object
*/
protected function get_legacy_factory() {
return $this->legacyfactory;
}
/**
* Execute the defined preprocessors on the DB record results and then convert
* them into entities.
*
* @param stdClass[] $records List of DB results
* @return array
*/
protected function transform_db_records_to_entities(array $records) {
$preprocessors = $this->get_preprocessors();
$result = array_map(function($record) {
return ['record' => $record];
}, $records);
$result = array_reduce(array_keys($preprocessors), function($carry, $preprocessor) use ($records, $preprocessors) {
$step = $preprocessors[$preprocessor];
$dependencies = $step->execute($records);
foreach ($dependencies as $index => $dependency) {
// Add the new dependency to the list.
$carry[$index] = array_merge($carry[$index], [$preprocessor => $dependency]);
}
return $carry;
}, $result);
return $this->from_db_records($result);
}
/**
* Get the entity for the given id.
*
* @param int $id Identifier for the entity
* @return object|null
*/
public function get_from_id(int $id) {
$records = $this->get_from_ids([$id]);
return count($records) ? array_shift($records) : null;
}
/**
* Get the list of entities for the given ids.
*
* @param int[] $ids Identifiers
* @return array
*/
public function get_from_ids(array $ids) {
$alias = $this->get_table_alias();
list($insql, $params) = $this->get_db()->get_in_or_equal($ids);
$wheresql = $alias . '.id ' . $insql;
$sql = $this->generate_get_records_sql($wheresql);
$records = $this->get_db()->get_records_sql($sql, $params);
return $this->transform_db_records_to_entities($records);
}
}
@@ -0,0 +1,175 @@
<?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/>.
/**
* Discussion vault class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\container;
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\discussion as discussion_entity;
/**
* Discussion vault class.
*
* 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
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class discussion extends db_table_vault {
/** The table for this vault */
private const TABLE = 'forum_discussions';
/**
* Get the table alias.
*
* @return string
*/
protected function get_table_alias(): string {
return 'd';
}
/**
* 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 user ID
* @return string
*/
protected function generate_get_records_sql(string $wheresql = null, string $sortsql = null, ?int $userid = null): string {
$selectsql = 'SELECT * FROM {' . self::TABLE . '} ' . $this->get_table_alias();
$selectsql .= $wheresql ? ' WHERE ' . $wheresql : '';
$selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : '';
return $selectsql;
}
/**
* Convert the DB records into discussion entities.
*
* @param array $results The DB records
* @return discussion_entity[]
*/
protected function from_db_records(array $results) {
$entityfactory = $this->get_entity_factory();
return array_map(function(array $result) use ($entityfactory) {
[
'record' => $record,
] = $result;
return $entityfactory->get_discussion_from_stdclass($record);
}, $results);
}
/**
* Get all discussions in the specified forum.
*
* @param forum_entity $forum
* @return array
*/
public function get_all_discussions_in_forum(forum_entity $forum, string $sort = null): ?array {
global $USER;
$options = ['forum' => $forum->get_id()];
$managerfactory = container::get_manager_factory();
$capabilitymanager = $managerfactory->get_capability_manager($forum);
$select = "forum = :forum";
if ($forum->is_in_group_mode() && !$capabilitymanager->can_access_all_groups($USER)) {
$allowedgroups = groups_get_activity_allowed_groups($forum->get_course_module_record());
$allowedgroups = implode(",", array_keys($allowedgroups));
if (!$allowedgroups) {
return [];
}
$select .= " AND groupid IN ($allowedgroups)";
}
$records = $this->get_db()->get_records_select(self::TABLE, $select, $options, $sort ?? '');
return $this->transform_db_records_to_entities($records);
}
/**
* Get the first discussion in the specified forum.
*
* @param forum_entity $forum
* @return discussion_entity|null
*/
public function get_first_discussion_in_forum(forum_entity $forum): ?discussion_entity {
$records = $this->get_db()->get_records(self::TABLE, [
'forum' => $forum->get_id(),
], 'timemodified ASC', '*', 0, 1);
$records = $this->transform_db_records_to_entities($records);
return count($records) ? array_shift($records) : null;
}
/**
* Get the last discussion in the specified forum.
*
* @param forum_entity $forum
* @return discussion_entity|null
*/
public function get_last_discussion_in_forum(forum_entity $forum): ?discussion_entity {
$records = $this->get_db()->get_records(self::TABLE, [
'forum' => $forum->get_id(),
], 'timemodified DESC', '*', 0, 1);
$records = $this->transform_db_records_to_entities($records);
return count($records) ? array_shift($records) : null;
}
/**
* Get the count of the discussions in the specified forum.
*
* @param forum_entity $forum
* @return int
*/
public function get_count_discussions_in_forum(forum_entity $forum): ?int {
return $this->get_db()->count_records(self::TABLE, [
'forum' => $forum->get_id()]);
}
/**
* Update the discussion
*
* @param discussion_entity $discussion
* @return discussion_entity|null
*/
public function update_discussion(discussion_entity $discussion): ?discussion_entity {
$discussionrecord = $this->get_legacy_factory()->to_legacy_object($discussion);
if ($this->get_db()->update_record('forum_discussions', $discussionrecord)) {
$records = $this->transform_db_records_to_entities([$discussionrecord]);
return count($records) ? array_shift($records) : null;
}
return null;
}
}
@@ -0,0 +1,564 @@
<?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_');
$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(\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];
}
}
+199
View File
@@ -0,0 +1,199 @@
<?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/>.
/**
* Forum vault class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\vaults\preprocessors\extract_context as extract_context_preprocessor;
use mod_forum\local\vaults\preprocessors\extract_record as extract_record_preprocessor;
use core\dml\table as dml_table;
use context_helper;
/**
* Forum vault class.
*
* 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
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class forum extends db_table_vault {
/** The table for this vault */
private const TABLE = 'forum';
/**
* Get the table alias.
*
* @return string
*/
protected function get_table_alias(): string {
return 'f';
}
/**
* 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 user ID
* @return string
*/
protected function generate_get_records_sql(string $wheresql = null, string $sortsql = null, ?int $userid = null): string {
$db = $this->get_db();
$alias = $this->get_table_alias();
$thistable = new dml_table(self::TABLE, $alias, $alias);
$cmtable = new dml_table('course_modules', 'cm', 'cm_');
$coursetable = new dml_table('course', 'c', 'c_');
$fields = implode(', ', [
$thistable->get_field_select(),
context_helper::get_preload_record_columns_sql('ctx'),
$cmtable->get_field_select(),
$coursetable->get_field_select(),
]);
$tables = $thistable->get_from_sql();
$tables .= " JOIN {modules} m ON m.name = 'forum'";
$tables .= " JOIN " . $cmtable->get_from_sql() . " ON cm.module = m.id AND cm.instance = {$alias}.id";
$tables .= ' JOIN {context} ctx ON ctx.contextlevel = ' . CONTEXT_MODULE . ' AND ctx.instanceid = cm.id';
$tables .= " JOIN " . $coursetable->get_from_sql() . " ON c.id = {$alias}.course";
$selectsql = 'SELECT ' . $fields . ' FROM ' . $tables;
$selectsql .= $wheresql ? ' WHERE ' . $wheresql : '';
$selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : '';
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(),
[
'forum' => new extract_record_preprocessor(self::TABLE, $this->get_table_alias()),
'course_module' => new extract_record_preprocessor('course_modules', 'cm_'),
'course' => new extract_record_preprocessor('course', 'c_'),
'context' => new extract_context_preprocessor(),
]
);
}
/**
* Convert the DB records into forum entities.
*
* @param array $results The DB records
* @return forum_entity[]
*/
protected function from_db_records(array $results): array {
$entityfactory = $this->get_entity_factory();
return array_map(function(array $result) use ($entityfactory) {
[
'forum' => $forumrecord,
'course_module' => $coursemodule,
'course' => $course,
'context' => $context,
] = $result;
return $entityfactory->get_forum_from_stdclass($forumrecord, $context, $coursemodule, $course);
}, $results);
}
/**
* Get the forum for the given course module id.
*
* @param int $id The course module id
* @return forum_entity|null
*/
public function get_from_course_module_id(int $id): ?forum_entity {
$records = $this->get_from_course_module_ids([$id]);
return count($records) ? array_shift($records) : null;
}
/**
* Get the forums for the given course module ids
*
* @param int[] $ids The course module ids
* @return forum_entity[]
*/
public function get_from_course_module_ids(array $ids): array {
$alias = $this->get_table_alias();
list($insql, $params) = $this->get_db()->get_in_or_equal($ids);
$wheresql = 'cm.id ' . $insql;
$sql = $this->generate_get_records_sql($wheresql);
$records = $this->get_db()->get_records_sql($sql, $params);
return $this->transform_db_records_to_entities($records);
}
/**
* Get the forum entity for the given post id.
*
* @param int $id The course module id
* @return forum_entity|null
*/
public function get_from_post_id(int $id): ?forum_entity {
$alias = $this->get_table_alias();
$thistable = new dml_table(self::TABLE, $alias, $alias);
$coursemoduletable = new dml_table('course_modules', 'cm', 'cm_');
$coursetable = new dml_table('course', 'c', 'c_');
$tablefields = $thistable->get_field_select();
$coursemodulefields = $coursemoduletable->get_field_select();
$coursefields = $coursetable->get_field_select();
$fields = implode(', ', [
$tablefields,
context_helper::get_preload_record_columns_sql('ctx'),
$coursemodulefields,
$coursefields,
]);
$tables = "{forum_posts} p";
$tables .= " JOIN {forum_discussions} d ON d.id = p.discussion";
$tables .= ' JOIN {' . self::TABLE . "} {$alias} ON {$alias}.id = d.forum";
$tables .= " JOIN {modules} m ON m.name = 'forum'";
$tables .= " JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = {$alias}.id";
$tables .= ' JOIN {context} ctx ON ctx.contextlevel = ' . CONTEXT_MODULE . ' AND ctx.instanceid = cm.id';
$tables .= " JOIN {course} c ON c.id = {$alias}.course";
$sql = "SELECT {$fields} FROM {$tables} WHERE p.id = :postid";
$records = $this->get_db()->get_records_sql($sql, [
'postid' => $id,
]);
$records = $this->transform_db_records_to_entities($records);
return count($records) ? array_shift($records) : null;
}
}
+551
View File
@@ -0,0 +1,551 @@
<?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/>.
/**
* Post vault class.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\forum as forum_entity;
use mod_forum\local\entities\post as post_entity;
use mod_forum\local\factories\entity as entity_factory;
use stdClass;
/**
* Post vault class.
*
* This should be the only place that accessed the database.
*
* This class should not return any objects other than post_entity objects. The class
* may contain some utility count methods which return integers.
*
* This uses the repository pattern. See:
* https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post extends db_table_vault {
/** The table for this vault */
private const TABLE = 'forum_posts';
/** Alias for user id */
private const USER_ID_ALIAS = 'userpictureid';
/** Alias for user fields */
private const USER_ALIAS = 'userrecord';
/**
* Get the table alias.
*
* @return string
*/
protected function get_table_alias(): string {
return 'p';
}
/**
* 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 user ID
* @return string
*/
protected function generate_get_records_sql(string $wheresql = null, string $sortsql = null, ?int $userid = null): string {
$table = self::TABLE;
$alias = $this->get_table_alias();
$fields = $alias . '.*';
$tables = "{{$table}} {$alias}";
$selectsql = "SELECT {$fields} FROM {$tables}";
$selectsql .= $wheresql ? ' WHERE ' . $wheresql : '';
$selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : '';
return $selectsql;
}
/**
* Convert the DB records into post entities.
*
* @param array $results The DB records
* @return post_entity[]
*/
protected function from_db_records(array $results) {
$entityfactory = $this->get_entity_factory();
return array_map(function(array $result) use ($entityfactory) {
['record' => $record] = $result;
return $entityfactory->get_post_from_stdclass($record);
}, $results);
}
/**
* Get the post ids for the given discussion.
*
* @param stdClass $user The user to check the unread count for
* @param int $discussionid The discussion to load posts for
* @param bool $canseeprivatereplies Whether this user can see all private replies or not
* @param string $orderby Order the results
* @return post_entity[]
*/
public function get_from_discussion_id(
stdClass $user,
int $discussionid,
bool $canseeprivatereplies,
string $orderby = 'created ASC'
): array {
return $this->get_from_discussion_ids($user, [$discussionid], $canseeprivatereplies, $orderby);
}
/**
* Get the list of posts for the given discussions.
*
* @param stdClass $user The user to load posts for.
* @param int[] $discussionids The list of discussion ids to load posts for
* @param bool $canseeprivatereplies Whether this user can see all private replies or not
* @param string $orderby Order the results
* @return post_entity[]
*/
public function get_from_discussion_ids(
stdClass $user,
array $discussionids,
bool $canseeprivatereplies,
string $orderby = ''
): array {
if (empty($discussionids)) {
return [];
}
return $this->get_from_filters($user, ['discussionids' => $discussionids], $canseeprivatereplies, $orderby);
}
/**
* The method returns posts based on a set of filters.
*
* @param stdClass $user Only used when restricting private replies
* @param array $filters Export filters, valid filters are:
*
* 'discussionids' => array of discussion ids eg [1,2,3]
* 'userids' => array of user ids eg [1,2,3]
* 'from' => timestamp to filter posts from this date.
* 'to' => timestamp to filter posts till this date.
*
* @param bool $canseeprivatereplies Whether this user can see all private replies or not
* @param string $orderby Order the results
* @return post_entity[]
*/
public function get_from_filters(
stdClass $user,
array $filters,
bool $canseeprivatereplies,
string $orderby = ''
): array {
if (count($filters) == 0) {
return [];
}
$wheresql = [];
$params = [];
$alias = $this->get_table_alias();
// Filter by discussion ids.
if (!empty($filters['discussionids'])) {
list($indiscussionssql, $indiscussionsparams) = $this->get_db()->get_in_or_equal($filters['discussionids'],
SQL_PARAMS_NAMED);
$wheresql[] = "{$alias}.discussion {$indiscussionssql}";
$params += $indiscussionsparams;
}
// Filter by user ids.
if (!empty($filters['userids'])) {
list($inuserssql, $inusersparams) = $this->get_db()->get_in_or_equal($filters['userids'],
SQL_PARAMS_NAMED);
$wheresql[] = "{$alias}.userid {$inuserssql}";
$params += $inusersparams;
}
// Filter posts by from and to dates.
if (isset($filters['from'])) {
$wheresql[] = "{$alias}.created >= :from";
$params['from'] = $filters['from'];
}
if (isset($filters['to'])) {
$wheresql[] = "{$alias}.created < :to";
$params['to'] = $filters['to'];
}
// We need to build the WHERE here, because get_private_reply_sql returns the query with the AND clause.
$wheresql = implode(' AND ', $wheresql);
// Build private replies sql.
[
'where' => $privatewhere,
'params' => $privateparams,
] = $this->get_private_reply_sql($user, $canseeprivatereplies);
$wheresql .= "{$privatewhere}";
$params += $privateparams;
if ($orderby) {
$orderbysql = $alias . '.' . $orderby;
} else {
$orderbysql = '';
}
$sql = $this->generate_get_records_sql($wheresql, $orderbysql);
$records = $this->get_db()->get_records_sql($sql, $params);
return $this->transform_db_records_to_entities($records);
}
/**
* Load a list of replies to the given post. This will load all descendants of the post.
* That is, all direct replies and replies to those replies etc.
*
* The return value will be a flat array of posts in the requested order.
*
* @param stdClass $user The user to check the unread count for
* @param post_entity $post The post to load replies for
* @param bool $canseeprivatereplies Whether this user can see all private replies or not
* @param string $orderby How to order the replies
* @return post_entity[]
*/
public function get_replies_to_post(
stdClass $user,
post_entity $post,
bool $canseeprivatereplies,
string $orderby = 'created ASC'
): array {
$alias = $this->get_table_alias();
[
'where' => $privatewhere,
'params' => $privateparams,
] = $this->get_private_reply_sql($user, $canseeprivatereplies);
$params = array_merge([
'discussionid' => $post->get_discussion_id(),
'created' => $post->get_time_created(),
'excludepostid' => $post->get_id(),
], $privateparams);
// Unfortunately the best we can do to filter down the query is ignore all posts
// that were created before the given post (since they can't be replies).
// We also filter to remove private replies if the user cannot vie them.
$wheresql = "{$alias}.discussion = :discussionid
AND {$alias}.created >= :created {$privatewhere}
AND {$alias}.id != :excludepostid";
$orderbysql = $alias . '.' . $orderby;
$sql = $this->generate_get_records_sql($wheresql, $orderbysql);
$records = $this->get_db()->get_records_sql($sql, $params);
$posts = $this->transform_db_records_to_entities($records);
$sorter = $this->get_entity_factory()->get_posts_sorter();
// We need to sort all of the values into the replies tree in order to capture
// the full list of descendants.
$sortedposts = $sorter->sort_into_children($posts);
$replies = [];
// From the sorted list we can grab the first elements and check if they are replies
// to the post we care about. If so we keep them.
foreach ($sortedposts as $candidate) {
[$candidatepost, $candidatereplies] = $candidate;
if ($candidatepost->has_parent() && $candidatepost->get_parent_id() == $post->get_id()) {
$replies[] = $candidate;
}
}
if (empty($replies)) {
return $replies;
}
$getreplypostids = function($candidates) use (&$getreplypostids) {
$ids = [];
foreach ($candidates as $candidate) {
[$reply, $replies] = $candidate;
$ids = array_merge($ids, [$reply->get_id()], $getreplypostids($replies));
}
return $ids;
};
// Recursively build a list of the ids of all posts in the full reply tree.
$replypostids = $getreplypostids($replies);
// Now go back and filter the original result set down to just the posts that
// we've flagged as in the reply tree. We need to filter the original set of values
// so that we can maintain the requested sort order.
return array_values(array_filter($posts, function($post) use ($replypostids) {
return in_array($post->get_id(), $replypostids);
}));
}
/**
* Get a mapping of replies to the specified discussions.
*
* @param stdClass $user The user to check the unread count for
* @param int[] $discussionids The list of discussions to fetch counts for
* @param bool $canseeprivatereplies Whether this user can see all private replies or not
* @return int[] The number of replies for each discussion returned in an associative array
*/
public function get_reply_count_for_discussion_ids(stdClass $user, array $discussionids, bool $canseeprivatereplies): array {
if (empty($discussionids)) {
return [];
}
list($insql, $params) = $this->get_db()->get_in_or_equal($discussionids, SQL_PARAMS_NAMED);
[
'where' => $privatewhere,
'params' => $privateparams,
] = $this->get_private_reply_sql($user, $canseeprivatereplies);
$sql = "SELECT discussion, COUNT(1)
FROM {" . self::TABLE . "} p
WHERE p.discussion {$insql} AND p.parent > 0 {$privatewhere}
GROUP BY discussion";
return $this->get_db()->get_records_sql_menu($sql, array_merge($params, $privateparams));
}
/**
* Get a mapping of replies to the specified discussions.
*
* @param stdClass $user The user to check the unread count for
* @param int $postid The post to collect replies to
* @param int $discussionid The list of discussions to fetch counts for
* @param bool $canseeprivatereplies Whether this user can see all private replies or not
* @return int The number of replies for each discussion returned in an associative array
*/
public function get_reply_count_for_post_id_in_discussion_id(
stdClass $user, int $postid, int $discussionid, bool $canseeprivatereplies): int {
[
'where' => $privatewhere,
'params' => $privateparams,
] = $this->get_private_reply_sql($user, $canseeprivatereplies);
$alias = $this->get_table_alias();
$table = self::TABLE;
$sql = "SELECT {$alias}.id, {$alias}.parent
FROM {{$table}} {$alias}
WHERE p.discussion = :discussionid {$privatewhere}";
$postparents = $this->get_db()->get_records_sql_menu($sql, array_merge([
'discussionid' => $discussionid,
], $privateparams));
return $this->count_children_from_parent_recursively($postparents, $postid);
}
/**
* Count the children whose parent matches the current record recursively.
*
* @param array $postparents The full mapping of posts.
* @param int $postid The ID to check for
* @return int $count
*/
private function count_children_from_parent_recursively(array $postparents, int $postid): int {
if (!isset($postparents[$postid])) {
// Post not found at all.
return 0;
}
$count = 0;
foreach ($postparents as $pid => $parentid) {
if ($postid == $parentid) {
$count += $this->count_children_from_parent_recursively($postparents, $pid) + 1;
}
}
return $count;
}
/**
* Get a mapping of unread post counts for the specified discussions.
*
* @param stdClass $user The user to fetch counts for
* @param int[] $discussionids The list of discussions to fetch counts for
* @param bool $canseeprivatereplies Whether this user can see all private replies or not
* @return int[] The count of unread posts for each discussion returned in an associative array
*/
public function get_unread_count_for_discussion_ids(stdClass $user, array $discussionids, bool $canseeprivatereplies): array {
global $CFG;
if (empty($discussionids)) {
return [];
}
[
'where' => $privatewhere,
'params' => $privateparams,
] = $this->get_private_reply_sql($user, $canseeprivatereplies);
$alias = $this->get_table_alias();
list($insql, $params) = $this->get_db()->get_in_or_equal($discussionids, SQL_PARAMS_NAMED);
$sql = "SELECT p.discussion, COUNT(p.id) FROM {" . self::TABLE . "} p
LEFT JOIN {forum_read} r ON r.postid = p.id AND r.userid = :userid
WHERE p.discussion {$insql} AND p.modified > :cutofftime AND r.id IS NULL {$privatewhere}
GROUP BY p.discussion";
$params['userid'] = $user->id;
$params['cutofftime'] = floor((new \DateTime())
->sub(new \DateInterval("P{$CFG->forum_oldpostdays}D"))
->format('U') / 60) * 60;
return $this->get_db()->get_records_sql_menu($sql, array_merge($params, $privateparams));
}
/**
* Get a mapping of the most recent post record in each discussion based on post creation time.
*
* @param stdClass $user
* @param array $discussionids
* @param bool $canseeprivatereplies
* @return array
* @throws \coding_exception
* @throws \dml_exception
*/
public function get_latest_posts_for_discussion_ids(
stdClass $user, array $discussionids, bool $canseeprivatereplies): array {
if (empty($discussionids)) {
return [];
}
list($insql, $params) = $this->get_db()->get_in_or_equal($discussionids, SQL_PARAMS_NAMED);
[
'where' => $privatewhere,
'params' => $privateparams,
] = $this->get_private_reply_sql($user, $canseeprivatereplies, "mp");
$sql = "
SELECT posts.*
FROM {" . self::TABLE . "} posts
JOIN (
SELECT p.discussion, MAX(p.id) as latestpostid
FROM {" . self::TABLE . "} p
JOIN (
SELECT mp.discussion, MAX(mp.created) AS created
FROM {" . self::TABLE . "} mp
WHERE mp.discussion {$insql} {$privatewhere}
GROUP BY mp.discussion
) lp ON lp.discussion = p.discussion AND lp.created = p.created
GROUP BY p.discussion
) plp on plp.discussion = posts.discussion AND plp.latestpostid = posts.id";
$records = $this->get_db()->get_records_sql($sql, array_merge($params, $privateparams));
$entities = $this->transform_db_records_to_entities($records);
return array_reduce($entities, function($carry, $entity) {
$carry[$entity->get_discussion_id()] = $entity;
return $carry;
}, []);
}
/**
* Get the SQL where and additional parameters to use to restrict posts to private reply posts.
*
* @param stdClass $user The user to fetch counts for
* @param bool $canseeprivatereplies Whether this user can see all private replies or not
* @return array The SQL WHERE clause, and parameters to use in the SQL.
*/
private function get_private_reply_sql(stdClass $user, bool $canseeprivatereplies, $posttablealias = "p") {
$params = [];
$privatewhere = '';
if (!$canseeprivatereplies) {
$privatewhere = " AND ({$posttablealias}.privatereplyto = :privatereplyto OR " .
"{$posttablealias}.userid = :privatereplyfrom OR {$posttablealias}.privatereplyto = 0)";
$params['privatereplyto'] = $user->id;
$params['privatereplyfrom'] = $user->id;
}
return [
'where' => $privatewhere,
'params' => $params,
];
}
/**
* Get a mapping of the first post in each discussion based on post creation time.
*
* @param int[] $discussionids The list of discussions to fetch counts for
* @return post_entity[] The post object of the first post for each discussions returned in an associative array
*/
public function get_first_post_for_discussion_ids(array $discussionids): array {
if (empty($discussionids)) {
return [];
}
list($insql, $params) = $this->get_db()->get_in_or_equal($discussionids, SQL_PARAMS_NAMED);
$sql = "
SELECT p.*
FROM {" . self::TABLE . "} p
JOIN (
SELECT mp.discussion, MIN(mp.created) AS created
FROM {" . self::TABLE . "} mp
WHERE mp.discussion {$insql}
GROUP BY mp.discussion
) lp ON lp.discussion = p.discussion AND lp.created = p.created";
$records = $this->get_db()->get_records_sql($sql, $params);
return $this->transform_db_records_to_entities($records);
}
/**
* Get the posts for the given user.
*
* @param int $discussionid The discussion to fetch posts for
* @param int $userid The user to fetch posts for
* @param bool $canseeprivatereplies Whether this user can see all private replies or not
* @param string $orderby Order the results
* @return post_entity[]
*/
public function get_posts_in_discussion_for_user_id(
int $discussionid,
int $userid,
bool $canseeprivatereplies,
string $orderby = 'created ASC'
): array {
$user = $this->get_db()->get_record('user', ['id' => (int)$userid], '*', IGNORE_MISSING);
$alias = $this->get_table_alias();
[
'where' => $privatewhere,
'params' => $privateparams,
] = $this->get_private_reply_sql($user, $canseeprivatereplies);
$wheresql = "{$alias}.userid = :authorid AND
{$alias}.discussion = :discussionid {$privatewhere}";
$orderbysql = $alias . '.' . $orderby;
$sql = $this->generate_get_records_sql($wheresql, $orderbysql);
$records = $this->get_db()->get_records_sql($sql, array_merge([
'authorid' => $userid,
'discussionid' => $discussionid
], $privateparams));
return $this->transform_db_records_to_entities($records);
}
}
@@ -0,0 +1,120 @@
<?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/>.
/**
* Post attachment vault class.
*
* @package mod_forum
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
use mod_forum\local\entities\post as post_entity;
use context;
use file_storage;
/**
* Post attachment vault class.
*
* 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
*
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post_attachment {
/** The component for attachments */
private const COMPONENT = 'mod_forum';
/** Sort the attachments by filename */
private const SORT = 'filename';
/** Don't include directories */
private const INCLUDE_DIRECTORIES = false;
/** @var file_storage $filestorage File storage */
private $filestorage;
/**
* Construct.
*
* @param file_storage $filestorage File storage
*/
public function __construct(file_storage $filestorage) {
$this->filestorage = $filestorage;
}
/**
* Get the attachments for the given posts. The results are indexed by
* post id.
*
* @param context $context The (forum) context that the posts are in
* @param post_entity[] $posts The list of posts to load attachments for
* @param string $area The file storage area, can be 'attachment' or 'post' for inline attachments.
* @return array Post attachments indexed by post id
*/
private function get_area_attachments_for_posts(context $context, array $posts, string $area) {
$itemids = array_map(function($post) {
return $post->get_id();
}, $posts);
$files = $this->filestorage->get_area_files(
$context->id,
self::COMPONENT,
$area,
$itemids,
self::SORT,
self::INCLUDE_DIRECTORIES
);
$filesbyid = array_reduce($posts, function($carry, $post) {
$carry[$post->get_id()] = [];
return $carry;
}, []);
return array_reduce($files, function($carry, $file) {
$itemid = $file->get_itemid();
$carry[$itemid] = array_merge($carry[$itemid], [$file]);
return $carry;
}, $filesbyid);
}
/**
* Get attachment for posts.
*
* @param context $context The (forum) context that the posts are in
* @param post_entity[] $posts The list of posts to load attachments for
* @return array Post attachments indexed by post id
*/
public function get_attachments_for_posts(context $context, array $posts) {
return $this->get_area_attachments_for_posts($context, $posts, 'attachment');
}
/**
* Get inline attachments for posts.
*
* @param context $context The (forum) context that the posts are in
* @param post_entity[] $posts The list of posts to load attachments for
* @return array Post attachments indexed by post id
*/
public function get_inline_attachments_for_posts(context $context, array $posts) {
return $this->get_area_attachments_for_posts($context, $posts, 'post');
}
}
@@ -0,0 +1,121 @@
<?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/>.
/**
* Post read receipt collection class.
*
* @package mod_forum
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults;
defined('MOODLE_INTERNAL') || die();
use stdClass;
/**
* Post read receipt collection class.
*
* 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
*
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class post_read_receipt_collection extends db_table_vault {
/** The table for this vault */
private const TABLE = 'forum_read';
/**
* Get the table alias.
*
* @return string
*/
protected function get_table_alias(): string {
return 'fr';
}
/**
* 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 user ID
* @return string
*/
protected function generate_get_records_sql(string $wheresql = null, string $sortsql = null, ?int $userid = null): string {
$selectsql = 'SELECT * FROM {' . self::TABLE . '} ' . $this->get_table_alias();
$selectsql .= $wheresql ? ' WHERE ' . $wheresql : '';
$selectsql .= $sortsql ? ' ORDER BY ' . $sortsql : '';
return $selectsql;
}
/**
* Convert the DB records into post_read_receipt_collection entities.
*
* @param array $results The DB records
* @return post_read_receipt_collection
*/
protected function from_db_records(array $results) {
$entityfactory = $this->get_entity_factory();
$records = array_map(function($result) {
return $result['record'];
}, $results);
return $entityfactory->get_post_read_receipt_collection_from_stdclasses($records);
}
/**
* Load the post_read_receipt_collection for the given user and set
* of posts.
*
* @param int $userid Id of the user to load receipts for
* @param int[] $postids List of post ids to load receipts for
* @return post_read_receipt_collection
*/
public function get_from_user_id_and_post_ids(int $userid, array $postids) {
$alias = $this->get_table_alias();
[$postidinsql, $params] = $this->get_db()->get_in_or_equal($postids);
$params[] = $userid;
$wheresql = "{$alias}.postid {$postidinsql}";
$wheresql .= " AND {$alias}.userid = ?";
$sql = $this->generate_get_records_sql($wheresql);
$records = $this->get_db()->get_records_sql($sql, $params);
return $this->transform_db_records_to_entities($records);
}
/**
* Load the post_read_receipt_collection for the given user and set
* of posts.
*
* @param stdClass $user The user to load receipts for
* @param post_entity[] $posts List of posts to load receipts for
* @return post_read_receipt_collection
*/
public function get_from_user_and_posts(stdClass $user, array $posts) {
$postids = array_map(function($post) {
return $post->get_id();
}, $posts);
return $this->get_from_user_id_and_post_ids($user->id, $postids);
}
}
@@ -0,0 +1,53 @@
<?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/>.
/**
* Extract context vault preprocessor.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults\preprocessors;
defined('MOODLE_INTERNAL') || die();
use context;
use context_helper;
/**
* Extract context vault preprocessor.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class extract_context {
/**
* Extract the contexts from a list of records.
*
* @param stdClass[] $records The list of records which have context properties
* @return context[] List of contexts matching the records.
*/
public function execute(array $records): array {
return array_map(function($record) {
$contextid = $record->ctxid;
context_helper::preload_from_record($record);
$context = context::instance_by_id($contextid);
return $context;
}, $records);
}
}
@@ -0,0 +1,67 @@
<?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/>.
/**
* Extract record vault preprocessor.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults\preprocessors;
defined('MOODLE_INTERNAL') || die();
use moodle_database;
use core\dml\table as dml_table;
/**
* Extract record vault preprocessor.
*
* Extract record vault preprocessor. Typically used to separate out records
* when two tables have been joined together in a query.
*
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class extract_record {
/** @var \core\dml\table $table The table object relating to the table that the records were loaded from */
private $table;
/**
* Constructor.
*
* @param string $table The table name where the records were loaded from
* @param string $alias The table alias used as the record property prefix
*/
public function __construct(string $table, string $alias) {
$this->table = new dml_table($table, $alias, $alias);
}
/**
* Extract a record embedded in the properties of another record out into a
* separate record.
*
* @param stdClass[] $records The list of records to process
* @return stdClass[] The extracted records
*/
public function execute(array $records): array {
return array_map(function($record) {
return $this->table->extract_from_result($record);
}, $records);
}
}
@@ -0,0 +1,71 @@
<?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/>.
/**
* Extract user vault preprocessor.
*
* @package mod_forum
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\local\vaults\preprocessors;
defined('MOODLE_INTERNAL') || die();
use user_picture;
/**
* Extract user vault preprocessor.
*
* Used to separate out the user record
* from a list of DB records that have been joined on the user table.
*
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class extract_user {
/** @var string $idalias The alias for the id property of the user */
private $idalias;
/** @var string $alias The prefix used for each of the user properties */
private $alias;
/**
* Constructor.
*
* @param string $idalias The alias for the id property of the user
* @param string $alias The prefix used for each of the user properties
*/
public function __construct(string $idalias, string $alias) {
$this->idalias = $idalias;
$this->alias = $alias;
}
/**
* Extract the user records from a list of DB records.
*
* @param stdClass[] $records The DB records
* @return stdClass[] The list of extracted users
*/
public function execute(array $records): array {
$idalias = $this->idalias;
$alias = $this->alias;
return array_map(function($record) use ($idalias, $alias) {
return user_picture::unalias($record, ['deleted'], $idalias, $alias);
}, $records);
}
}