Merge branch 'MDL-22077-master' of git://github.com/peterRd/moodle

This commit is contained in:
Adrian Greeve
2019-04-04 12:59:51 +08:00
53 changed files with 2924 additions and 597 deletions
@@ -32,6 +32,7 @@ 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;
@@ -67,6 +68,9 @@ class exported_discussion_summaries {
/** @var vault_factory $vaultfactory Vault factory */
private $vaultfactory;
/** @var manager_factory $managerfactory Manager factory */
private $managerfactory;
/** @var rating_manager $ratingmanager Rating manager */
private $ratingmanager;
@@ -77,20 +81,21 @@ class exported_discussion_summaries {
* @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
* @param manager_factory $managerfactory Manager factory
*/
public function __construct(
renderer_base $renderer,
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
rating_manager $ratingmanager
manager_factory $managerfactory
) {
$this->renderer = $renderer;
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->ratingmanager = $ratingmanager;
$this->managerfactory = $managerfactory;
$this->ratingmanager = $managerfactory->get_rating_manager();
}
/**
@@ -108,16 +113,18 @@ class exported_discussion_summaries {
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($discussionids);
$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($discussionids);
$latestposts = $postvault->get_latest_post_id_for_discussion_ids($discussionids);
$replycounts = $postvault->get_reply_count_for_discussion_ids($user, $discussionids, $canseeanyprivatereply);
$latestposts = $postvault->get_latest_post_id_for_discussion_ids($user, $discussionids, $canseeanyprivatereply);
$unreadcounts = [];
@@ -125,7 +132,7 @@ class exported_discussion_summaries {
$forumrecord = $forumdatamapper->to_legacy_object($forum);
if (forum_tp_can_track_forums($forumrecord)) {
$unreadcounts = $postvault->get_unread_count_for_discussion_ids($user, $discussionids);
$unreadcounts = $postvault->get_unread_count_for_discussion_ids($user, $discussionids, $canseeanyprivatereply);
}
$summaryexporter = $this->exporterfactory->get_discussion_summaries_exporter(
@@ -107,6 +107,9 @@ class exported_posts {
* 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
@@ -59,7 +59,8 @@ class post {
'attachment' => $post->has_attachments(),
'totalscore' => $post->get_total_score(),
'mailnow' => $post->should_mail_now(),
'deleted' => $post->is_deleted()
'deleted' => $post->is_deleted(),
'privatereplyto' => $post->get_private_reply_recipient_id(),
];
}, $posts);
}
+35 -1
View File
@@ -65,6 +65,8 @@ class post {
private $mailnow;
/** @var bool $deleted Is the post deleted */
private $deleted;
/** @var int $privatereplyto The user being privately replied to */
private $privatereplyto;
/**
* Constructor.
@@ -84,6 +86,7 @@ class post {
* @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,
@@ -100,7 +103,8 @@ class post {
bool $hasattachments,
int $totalscore,
bool $mailnow,
bool $deleted
bool $deleted,
int $privatereplyto
) {
$this->id = $id;
$this->discussionid = $discussionid;
@@ -117,6 +121,7 @@ class post {
$this->totalscore = $totalscore;
$this->mailnow = $mailnow;
$this->deleted = $deleted;
$this->privatereplyto = $privatereplyto;
}
/**
@@ -263,6 +268,25 @@ class post {
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.
*
@@ -281,4 +305,14 @@ class post {
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;
}
}
+4 -13
View File
@@ -27,6 +27,7 @@ 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;
@@ -81,31 +82,21 @@ class author extends exporter {
'null' => NULL_ALLOWED
],
'groups' => [
'type' => group_exporter::read_properties_definition(),
'multiple' => true,
'optional' => true,
'type' => [
'id' => ['type' => PARAM_INT],
'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,
+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 <[email protected]>
* @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 <[email protected]>
* @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'
];
}
}
+89 -16
View File
@@ -70,6 +70,7 @@ class post extends exporter {
'type' => [
'export' => [
'type' => PARAM_URL,
'description' => 'The URL used to export the attachment',
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
@@ -80,6 +81,7 @@ class post extends exporter {
'type' => [
'plagiarism' => [
'type' => PARAM_RAW,
'description' => 'The HTML source for the Plagiarism Response',
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED
@@ -109,6 +111,7 @@ class post extends exporter {
'null' => NULL_ALLOWED
],
'isdeleted' => ['type' => PARAM_BOOL],
'isprivatereply' => ['type' => PARAM_BOOL],
'haswordcount' => ['type' => PARAM_BOOL],
'wordcount' => [
'type' => PARAM_INT,
@@ -118,13 +121,41 @@ class post extends exporter {
],
'capabilities' => [
'type' => [
'view' => ['type' => PARAM_BOOL],
'edit' => ['type' => PARAM_BOOL],
'delete' => ['type' => PARAM_BOOL],
'split' => ['type' => PARAM_BOOL],
'reply' => ['type' => PARAM_BOOL],
'export' => ['type' => PARAM_BOOL],
'controlreadstatus' => ['type' => PARAM_BOOL]
'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',
],
'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',
],
]
],
'urls' => [
@@ -133,60 +164,71 @@ class post extends exporter {
'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,
@@ -210,14 +252,40 @@ class post extends exporter {
'null' => NULL_ALLOWED,
'multiple' => true,
'type' => [
'id' => ['type' => PARAM_INT],
'tagid' => ['type' => PARAM_INT],
'isstandard' => ['type' => PARAM_BOOL],
'displayname' => ['type' => PARAM_TEXT],
'flag' => ['type' => PARAM_BOOL],
'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]
'view' => [
'type' => PARAM_URL,
'description' => 'The URL to view the tag',
'null' => NULL_NOT_ALLOWED,
],
]
]
]
@@ -231,19 +299,22 @@ class post extends exporter {
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'type' => PARAM_RAW
'type' => PARAM_RAW,
'description' => 'The HTML source to rate the post',
],
'taglist' => [
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'type' => PARAM_RAW
'type' => PARAM_RAW,
'description' => 'The HTML source to view the list of tags',
],
'authorsubheading' => [
'optional' => true,
'default' => null,
'null' => NULL_ALLOWED,
'type' => PARAM_RAW
'type' => PARAM_RAW,
'description' => 'The HTML source to view the author details',
],
]
]
@@ -269,6 +340,7 @@ class post extends exporter {
$attachments = $this->related['attachments'];
$includehtml = $this->related['includehtml'];
$isdeleted = $post->is_deleted();
$isprivatereply = $post->is_private_reply();
$hasrating = $rating != null;
$hastags = !empty($tags);
$discussionid = $post->get_discussion_id();
@@ -328,6 +400,7 @@ class post extends exporter {
'timecreated' => $timecreated,
'unread' => ($loadcontent && $readreceiptcollection) ? !$readreceiptcollection->has_user_read_post($user, $post) : null,
'isdeleted' => $isdeleted,
'isprivatereply' => $isprivatereply,
'haswordcount' => $forum->should_display_word_count(),
'wordcount' => $forum->should_display_word_count() ? count_words($message) : null,
'capabilities' => [
@@ -105,7 +105,7 @@ class builder {
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$this->managerfactory->get_rating_manager()
$this->managerfactory
);
}
}
+2 -1
View File
@@ -150,7 +150,8 @@ class entity {
$record->attachment,
$record->totalscore,
$record->mailnow,
$record->deleted
$record->deleted,
$record->privatereplyto
);
}
@@ -431,7 +431,7 @@ class renderer {
$this->urlfactory,
$template,
$notifications,
function($discussions, $user, $forum) {
function($discussions, $user, $forum) use ($capabilitymanager) {
$exportedpostsbuilder = $this->builderfactory->get_exported_posts_builder();
$discussionentries = [];
$postentries = [];
@@ -449,7 +449,12 @@ class renderer {
);
$postvault = $this->vaultfactory->get_post_vault();
$discussionrepliescount = $postvault->get_reply_count_for_discussion_ids($discussionentriesids);
$canseeanyprivatereply = $capabilitymanager->can_view_any_private_reply($user);
$discussionrepliescount = $postvault->get_reply_count_for_discussion_ids(
$user,
$discussionentriesids,
$canseeanyprivatereply
);
array_walk($exportedposts['posts'], function($post) use ($discussionrepliescount) {
$post->discussionrepliescount = $discussionrepliescount[$post->discussionid] ?? 0;
@@ -316,7 +316,7 @@ class capability {
}
/**
* Can the user view the post in this discussion?
* 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
@@ -324,6 +324,10 @@ class capability {
* @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;
}
$forum = $this->get_forum();
$forumrecord = $this->get_forum_record();
$discussionrecord = $this->get_discussion_record($discussion);
@@ -332,6 +336,37 @@ class capability {
return forum_user_can_see_post($forumrecord, $discussionrecord, $postrecord, $user, $coursemodule, false);
}
/**
* 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_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?
*
@@ -394,6 +429,11 @@ class capability {
* @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();
}
@@ -406,9 +446,30 @@ class capability {
* @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;
}
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?
*
+40
View File
@@ -155,4 +155,44 @@ class forum extends db_table_vault {
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;
}
}
+165 -20
View File
@@ -96,16 +96,32 @@ class post extends db_table_vault {
/**
* 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(int $discussionid, string $orderby = 'created ASC') : array {
public function get_from_discussion_id(
stdClass $user,
int $discussionid,
bool $canseeprivatereplies,
string $orderby = 'created ASC'
) : array {
$alias = $this->get_table_alias();
$wheresql = $alias . '.discussion = ?';
[
'where' => $privatewhere,
'params' => $privateparams,
] = $this->get_private_reply_sql($user, $canseeprivatereplies);
$wheresql = "{$alias}.discussion = :discussionid {$privatewhere}";
$orderbysql = $alias . '.' . $orderby;
$sql = $this->generate_get_records_sql($wheresql, $orderbysql);
$records = $this->get_db()->get_records_sql($sql, [$discussionid]);
$records = $this->get_db()->get_records_sql($sql, array_merge([
'discussionid' => $discussionid,
], $privateparams));
return $this->transform_db_records_to_entities($records);
}
@@ -113,22 +129,28 @@ class post extends db_table_vault {
/**
* Get the list of posts for the given discussions.
*
* @param stdClass $user The user to check the unread count 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
* @return post_entity[]
*/
public function get_from_discussion_ids(array $discussionids) : array {
public function get_from_discussion_ids(stdClass $user, array $discussionids, bool $canseeprivatereplies) : array {
if (empty($discussionids)) {
return [];
}
$alias = $this->get_table_alias();
list($insql, $params) = $this->get_db()->get_in_or_equal($discussionids);
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);
$wheresql = "{$alias}.discussion {$insql}";
$wheresql = "{$alias}.discussion {$insql} {$privatewhere}";
$sql = $this->generate_get_records_sql($wheresql, '');
$records = $this->get_db()->get_records_sql($sql, $params);
$records = $this->get_db()->get_records_sql($sql, array_merge($params, $privateparams));
return $this->transform_db_records_to_entities($records);
}
@@ -139,21 +161,43 @@ class post extends db_table_vault {
*
* 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(post_entity $post, string $orderby = 'created ASC') : array {
public function get_replies_to_post(
stdClass $user,
post_entity $post,
bool $canseeprivatereplies,
string $orderby = 'created ASC'
) : array {
$alias = $this->get_table_alias();
$params = [$post->get_discussion_id(), $post->get_time_created(), $post->get_id()];
[
'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).
$wheresql = "{$alias}.discussion = ? and {$alias}.created >= ? and {$alias}.id != ?";
// 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);
@@ -196,18 +240,82 @@ class post extends db_table_vault {
/**
* 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(array $discussionids) : 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 = "SELECT discussion, COUNT(1) FROM {" . self::TABLE . "} p " .
"WHERE p.discussion {$insql} AND p.parent > 0 GROUP BY discussion";
return $this->get_db()->get_records_sql_menu($sql, $params);
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;
}
/**
@@ -215,20 +323,26 @@ class post extends db_table_vault {
*
* @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) : 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
WHERE p.discussion {$insql} AND p.modified > :cutofftime AND r.id IS NULL {$privatewhere}
GROUP BY p.discussion";
$params['userid'] = $user->id;
@@ -236,16 +350,19 @@ class post extends db_table_vault {
->sub(new \DateInterval("P{$CFG->forum_oldpostdays}D"))
->format('U') / 60) * 60;
return $this->get_db()->get_records_sql_menu($sql, $params);
return $this->get_db()->get_records_sql_menu($sql, array_merge($params, $privateparams));
}
/**
* Get a mapping of the most recent post in each discussion based on post creation time.
*
* @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 post id of the most recent post for each discussions returned in an associative array
*/
public function get_latest_post_id_for_discussion_ids(array $discussionids) : array {
public function get_latest_post_id_for_discussion_ids(
stdClass $user, array $discussionids, bool $canseeprivatereplies) : array {
global $CFG;
if (empty($discussionids)) {
@@ -255,6 +372,11 @@ class post extends db_table_vault {
$alias = $this->get_table_alias();
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 p.discussion, MAX(p.id)
FROM {" . self::TABLE . "} p
@@ -264,8 +386,31 @@ class post extends db_table_vault {
WHERE mp.discussion {$insql}
GROUP BY mp.discussion
) lp ON lp.discussion = p.discussion AND lp.created = p.created
WHERE 1 = 1 {$privatewhere}
GROUP BY p.discussion";
return $this->get_db()->get_records_sql_menu($sql, $params);
return $this->get_db()->get_records_sql_menu($sql, array_merge($params, $privateparams));
}
/**
* 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) {
$params = [];
$privatewhere = '';
if (!$canseeprivatereplies) {
$privatewhere = ' AND (p.privatereplyto = :privatereplyto OR p.userid = :privatereplyfrom OR p.privatereplyto = 0)';
$params['privatereplyto'] = $user->id;
$params['privatereplyfrom'] = $user->id;
}
return [
'where' => $privatewhere,
'params' => $params,
];
}
}
+16 -2
View File
@@ -152,7 +152,8 @@ class forum_post implements \renderable, \templatable {
* @return array Data ready for use in a mustache template
*/
protected function export_for_template_text(\mod_forum_renderer $renderer) {
return array(
$data = $this->export_for_template_shared($renderer);
return $data + array(
'id' => html_entity_decode($this->post->id),
'coursename' => html_entity_decode($this->get_coursename()),
'courselink' => html_entity_decode($this->get_courselink()),
@@ -193,7 +194,8 @@ class forum_post implements \renderable, \templatable {
* @return array Data ready for use in a mustache template
*/
protected function export_for_template_html(\mod_forum_renderer $renderer) {
return array(
$data = $this->export_for_template_shared($renderer);
return $data + array(
'id' => $this->post->id,
'coursename' => $this->get_coursename(),
'courselink' => $this->get_courselink(),
@@ -207,7 +209,17 @@ class forum_post implements \renderable, \templatable {
// Format some components according to the renderer.
'message' => $renderer->format_message_text($this->cm, $this->post),
'attachments' => $renderer->format_message_attachments($this->cm, $this->post),
);
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param \mod_forum_renderer $renderer The render to be used for formatting the message and attachments
* @return stdClass Data ready for use in a mustache template
*/
protected function export_for_template_shared(\mod_forum_renderer $renderer) {
return array(
'canreply' => $this->canreply,
'permalink' => $this->get_permalink(),
'firstpost' => $this->get_is_firstpost(),
@@ -224,6 +236,8 @@ class forum_post implements \renderable, \templatable {
'authorpicture' => $this->get_author_picture($renderer),
'grouppicture' => $this->get_group_picture($renderer),
'isprivatereply' => !empty($this->post->privatereplyto),
);
}
+12
View File
@@ -93,6 +93,7 @@ class mod_forum_post_form extends moodleform {
$subscribe = $this->_customdata['subscribe'];
$edit = $this->_customdata['edit'];
$thresholdwarning = $this->_customdata['thresholdwarning'];
$canreplyprivately = $this->_customdata['canreplyprivately'];
$mform->addElement('header', 'general', '');//fill in the data depending on page params later using set_data
@@ -147,6 +148,17 @@ class mod_forum_post_form extends moodleform {
$mform->addElement('checkbox', 'mailnow', get_string('mailnow', 'forum'));
}
if ((empty($post->id) && $canreplyprivately) || (!empty($post) && !empty($post->privatereplyto))) {
// Only show the option to change private reply settings if this is a new post and the user can reply
// privately, or if this is already private reply, in which case the state is shown but is not editable.
$mform->addElement('checkbox', 'isprivatereply', get_string('privatereply', 'forum'));
$mform->addHelpButton('isprivatereply', 'privatereply', 'forum');
if (!empty($post->privatereplyto)) {
$mform->setDefault('isprivatereply', 1);
$mform->freeze('isprivatereply');
}
}
if ($groupmode = groups_get_activity_groupmode($cm, $course)) {
$groupdata = groups_get_activity_allowed_groups($cm);
+15
View File
@@ -96,6 +96,7 @@ class provider implements
'subject' => 'privacy:metadata:forum_posts:subject',
'message' => 'privacy:metadata:forum_posts:message',
'userid' => 'privacy:metadata:forum_posts:userid',
'privatereplyto' => 'privacy:metadata:forum_posts:privatereplyto',
], 'privacy:metadata:forum_posts');
// The 'forum_queue' table contains user data, but it is only a temporary cache of other data.
@@ -620,6 +621,7 @@ class provider implements
WHERE f.id ${foruminsql} AND
(
p.userid = :postuserid OR
p.privatereplyto = :privatereplyrecipient OR
fr.id IS NOT NULL OR
{$ratingsql->userwhere}
)
@@ -629,6 +631,7 @@ class provider implements
$params = [
'postuserid' => $userid,
'readuserid' => $userid,
'privatereplyrecipient' => $userid,
];
$params += $forumparams;
$params += $ratingsql->params;
@@ -666,11 +669,18 @@ class provider implements
LEFT JOIN {forum_read} fr ON fr.postid = p.id AND fr.userid = :readuserid
{$ratingsql->join} AND {$ratingsql->userwhere}
WHERE d.id = :discussionid
AND (
p.privatereplyto = 0
OR p.privatereplyto = :privatereplyrecipient
OR p.userid = :privatereplyauthor
)
";
$params = [
'discussionid' => $discussionid,
'readuserid' => $userid,
'privatereplyrecipient' => $userid,
'privatereplyauthor' => $userid,
];
$params += $ratingsql->params;
@@ -685,6 +695,7 @@ class provider implements
$post->hasdata = $post->hasdata || !empty($post->hasratings);
$post->hasdata = $post->hasdata || $post->readflag;
$post->hasdata = $post->hasdata || ($post->userid == $USER->id);
$post->hasdata = $post->hasdata || ($post->privatereplyto == $USER->id);
if (0 == $post->parent) {
$structure->children[$post->id] = $post;
@@ -756,6 +767,10 @@ class provider implements
'author_was_you' => transform::yesno($post->userid == $userid),
];
if (!empty($post->privatereplyto)) {
$postdata->privatereply = transform::yesno(true);
}
$postdata->message = writer::with_context($context)
->rewrite_pluginfile_urls($postarea, 'mod_forum', 'post', $post->id, $post->message);
+20
View File
@@ -165,6 +165,26 @@ $capabilities = array(
)
),
'mod/forum:postprivatereply' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => array(
'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW
)
),
'mod/forum:readprivatereplies' => array(
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => array(
'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW
)
),
'mod/forum:createattachment' => array(
'riskbitmask' => RISK_SPAM,
+3 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/forum/db" VERSION="20180329" COMMENT="XMLDB file for Moodle mod/forum"
<XMLDB PATH="mod/forum/db" VERSION="20190311" COMMENT="XMLDB file for Moodle mod/forum"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
@@ -81,6 +81,7 @@
<FIELD NAME="totalscore" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="mailnow" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="deleted" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="privatereplyto" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
@@ -188,4 +189,4 @@
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
</XMLDB>
+10
View File
@@ -37,6 +37,16 @@ $functions = array(
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_forum_get_discussion_posts' => array(
'classname' => 'mod_forum_external',
'methodname' => 'get_discussion_posts',
'classpath' => 'mod/forum/externallib.php',
'description' => 'Returns a list of forum posts for a discussion.',
'type' => 'read',
'capabilities' => 'mod/forum:viewdiscussion, mod/forum:viewqandawithoutposting',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_forum_get_forum_discussion_posts' => array(
'classname' => 'mod_forum_external',
'methodname' => 'get_forum_discussion_posts',
+14
View File
@@ -104,5 +104,19 @@ function xmldb_forum_upgrade($oldversion) {
// Automatically generated Moodle v3.6.0 release upgrade line.
// Put any upgrade step following this.
if ($oldversion < 2019031200) {
// Define field privatereplyto to be added to forum_posts.
$table = new xmldb_table('forum_posts');
$field = new xmldb_field('privatereplyto', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'mailnow');
// Conditionally launch add field privatereplyto.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Forum savepoint reached.
upgrade_mod_savepoint(true, 2019031200, 'forum');
}
return true;
}
+32 -1
View File
@@ -1440,7 +1440,7 @@ function forum_print_latest_discussions($course, $forum, $maxdiscussions = -1, $
$getuserlastmodified = ($displayformat == 'header');
$discussions = forum_get_discussions($cm, $sort, $fullpost, null, $maxdiscussions, $getuserlastmodified, $page, $perpage);
if (!$$discussions) {
if (!$discussions) {
echo '<div class="forumnodiscuss">';
if ($forum->type == 'news') {
echo '('.get_string('nonews', 'forum').')';
@@ -1620,3 +1620,34 @@ function forum_print_latest_discussions($course, $forum, $maxdiscussions = -1, $
echo $OUTPUT->paging_bar($numdiscussions, $page, $perpage, "view.php?f=$forum->id");
}
}
/**
* Count the number of replies to the specified post.
*
* @param object $post
* @param bool $children
* @return int
* @deprecated since Moodle 3.7
* @todo MDL-65252 This will be removed in Moodle 4.1
*/
function forum_count_replies($post, $children = true) {
global $USER;
debugging('forum_count_replies has been deprecated. Please use the Post vault instead.', DEBUG_DEVELOPER);
if (!$children) {
return $DB->count_records('forum_posts', array('parent' => $post->id));
}
$entityfactory = mod_forum\local\container::get_entity_factory();
$postentity = $entityfactory->get_post_from_stdclass($post);
$vaultfactory = mod_forum\local\container::get_vault_factory();
$postvault = $vaultfactory->get_post_vault();
return $postvault->get_reply_count_for_post_id_in_discussion_id(
$USER,
$postentity->get_id(),
$postentity->get_discussion_id(),
true
);
}
+1 -1
View File
@@ -302,7 +302,7 @@ echo $OUTPUT->heading(format_string($discussion->get_name()), 3, 'discussionname
$rendererfactory = mod_forum\local\container::get_renderer_factory();
$discussionrenderer = $rendererfactory->get_discussion_renderer($forum, $discussion, $displaymode);
$orderpostsby = $displaymode == FORUM_MODE_FLATNEWEST ? 'created DESC' : 'created ASC';
$replies = $postvault->get_replies_to_post($post, $orderpostsby);
$replies = $postvault->get_replies_to_post($USER, $post, $capabilitymanager->can_view_any_private_reply($USER), $orderpostsby);
$postids = array_map(function($post) {
return $post->get_id();
}, array_merge([$post], array_values($replies)));
+119 -1
View File
@@ -156,6 +156,103 @@ class mod_forum_external extends external_api {
);
}
/**
* Get the forum posts in the specified discussion.
*
* @param int $discussionid
* @param string $sortby
* @param string $sortdirection
* @return array
*/
public static function get_discussion_posts(int $discussionid, ?string $sortby, ?string $sortdirection) {
global $USER;
// Validate the parameter.
$params = self::validate_parameters(self::get_discussion_posts_parameters(), [
'discussionid' => $discussionid,
'sortby' => $sortby,
'sortdirection' => $sortdirection,
]);
$warnings = [];
$vaultfactory = mod_forum\local\container::get_vault_factory();
$discussionvault = $vaultfactory->get_discussion_vault();
$discussion = $discussionvault->get_from_id($params['discussionid']);
$forumvault = $vaultfactory->get_forum_vault();
$forum = $forumvault->get_from_id($discussion->get_forum_id());
$sortby = $params['sortby'];
$sortdirection = $params['sortdirection'];
$sortallowedvalues = ['id', 'created', 'modified'];
$directionallowedvalues = ['ASC', 'DESC'];
if (!in_array(strtolower($sortby), $sortallowedvalues)) {
throw new invalid_parameter_exception('Invalid value for sortby parameter (value: ' . $sortby . '),' .
'allowed values are: ' . implode(', ', $sortallowedvalues));
}
$sortdirection = strtoupper($sortdirection);
if (!in_array($sortdirection, $directionallowedvalues)) {
throw new invalid_parameter_exception('Invalid value for sortdirection parameter (value: ' . $sortdirection . '),' .
'allowed values are: ' . implode(',', $directionallowedvalues));
}
$managerfactory = mod_forum\local\container::get_manager_factory();
$capabilitymanager = $managerfactory->get_capability_manager($forum);
$postvault = $vaultfactory->get_post_vault();
$posts = $postvault->get_from_discussion_id(
$USER,
$discussion->get_id(),
$capabilitymanager->can_view_any_private_reply($USER),
"{$sortby} {$sortdirection}"
);
$builderfactory = mod_forum\local\container::get_builder_factory();
$postbuilder = $builderfactory->get_exported_posts_builder();
$legacydatamapper = mod_forum\local\container::get_legacy_data_mapper_factory();
return [
'posts' => $postbuilder->build($USER, [$forum], [$discussion], $posts),
'ratinginfo' => \core_rating\external\util::get_rating_info(
$legacydatamapper->get_forum_data_mapper()->to_legacy_object($forum),
$forum->get_context(),
'mod_forum',
'post',
$legacydatamapper->get_post_data_mapper()->to_legacy_objects($posts)
),
'warnings' => $warnings,
];
}
/**
* Describe the post parameters.
*
* @return external_function_parameters
*/
public static function get_discussion_posts_parameters() {
return new external_function_parameters ([
'discussionid' => new external_value(PARAM_INT, 'The ID of the discussion from which to fetch posts.', VALUE_REQUIRED),
'sortby' => new external_value(PARAM_ALPHA, 'Sort by this element: id, created or modified', VALUE_DEFAULT, 'created'),
'sortdirection' => new external_value(PARAM_ALPHA, 'Sort direction: ASC or DESC', VALUE_DEFAULT, 'DESC')
]);
}
/**
* Describe the post return format.
*
* @return external_single_structure
*/
public static function get_discussion_posts_returns() {
return new external_single_structure([
'posts' => new external_multiple_structure(\mod_forum\local\exporters\post::get_read_structure()),
'ratinginfo' => \core_rating\external\util::external_ratings_structure(),
'warnings' => new external_warnings()
]);
}
/**
* Describes the parameters for get_forum_discussion_posts.
*
@@ -182,6 +279,7 @@ class mod_forum_external extends external_api {
*
* @return array the forum post details
* @since Moodle 2.7
* @todo MDL-65252 This will be removed in Moodle 4.1
*/
public static function get_forum_discussion_posts($discussionid, $sortby = "created", $sortdirection = "DESC") {
global $CFG, $DB, $USER, $PAGE;
@@ -267,6 +365,8 @@ class mod_forum_external extends external_api {
$post->postread = true;
}
$post->isprivatereply = !empty($post->privatereplyto);
$post->canreply = $canreply;
if (!empty($post->children)) {
$post->children = array_keys($post->children);
@@ -364,6 +464,7 @@ class mod_forum_external extends external_api {
'userfullname' => new external_value(PARAM_TEXT, 'Post author full name'),
'userpictureurl' => new external_value(PARAM_URL, 'Post author picture.', VALUE_OPTIONAL),
'deleted' => new external_value(PARAM_BOOL, 'This post has been removed.'),
'isprivatereply' => new external_value(PARAM_BOOL, 'The post is a private reply'),
), 'post'
)
),
@@ -373,6 +474,15 @@ class mod_forum_external extends external_api {
);
}
/**
* Mark the get_forum_discussion_posts web service as deprecated.
*
* @return bool
*/
public static function get_forum_discussion_posts_is_deprecated() {
return true;
}
/**
* Describes the parameters for get_forum_discussions_paginated.
*
@@ -468,7 +578,8 @@ class mod_forum_external extends external_api {
}
}
// The forum function returns the replies for all the discussions in a given forum.
$replies = forum_count_discussion_replies($forumid, $sort, -1, $page, $perpage);
$canseeprivatereplies = has_capability('mod/forum:readprivatereplies', $modcontext);
$replies = forum_count_discussion_replies($forumid, $sort, -1, $page, $perpage, $canseeprivatereplies);
foreach ($alldiscussions as $discussion) {
@@ -771,6 +882,7 @@ class mod_forum_external extends external_api {
'name' => new external_value(PARAM_ALPHANUM,
'The allowed keys (value format) are:
discussionsubscribe (bool); subscribe to the discussion?, default to true
private (bool); make this reply private to the author of the parent post, default to false.
inlineattachmentsid (int); the draft file area id for inline attachments
attachmentsid (int); the draft file area id for attachments
'),
@@ -806,6 +918,7 @@ class mod_forum_external extends external_api {
'options' => $options
)
);
$warnings = array();
if (!$parent = forum_get_post_full($params['postid'])) {
@@ -826,6 +939,7 @@ class mod_forum_external extends external_api {
// Validate options.
$options = array(
'discussionsubscribe' => true,
'private' => false,
'inlineattachmentsid' => 0,
'attachmentsid' => null
);
@@ -835,6 +949,9 @@ class mod_forum_external extends external_api {
case 'discussionsubscribe':
$value = clean_param($option['value'], PARAM_BOOL);
break;
case 'private':
$value = clean_param($option['value'], PARAM_BOOL);
break;
case 'inlineattachmentsid':
$value = clean_param($option['value'], PARAM_INT);
break;
@@ -868,6 +985,7 @@ class mod_forum_external extends external_api {
$post->messagetrust = trusttext_trusted($context);
$post->itemid = $options['inlineattachmentsid'];
$post->attachments = $options['attachmentsid'];
$post->isprivatereply = $options['private'];
$post->deleted = 0;
$fakemform = $post->attachments;
if ($postid = forum_add_new_post($post, $fakemform)) {
+7
View File
@@ -60,6 +60,7 @@ $string['cannotaddteacherforumto'] = 'Could not add converted teacher forum inst
$string['cannotcreatediscussion'] = 'Could not create new discussion';
$string['cannotcreateinstanceforteacher'] = 'Could not create new course module instance for the teacher forum';
$string['cannotdeletepost'] = 'You can\'t delete this post!';
$string['cannotdeletediscussioninsinglediscussion'] = 'You cannot delete the first post in a single discussion';
$string['cannoteditposts'] = 'You can\'t edit other people\'s posts!';
$string['cannotfinddiscussion'] = 'Could not find the discussion in this forum';
$string['cannotfindfirstpost'] = 'Could not find the first post in this forum';
@@ -248,6 +249,8 @@ $string['forumposts'] = 'Forum posts';
$string['forum:rate'] = 'Rate posts';
$string['forum:replynews'] = 'Reply to announcements';
$string['forum:replypost'] = 'Reply to posts';
$string['forum:postprivatereply'] = 'Reply privately to posts';
$string['forum:readprivatereplies'] = 'View private replies';
$string['forums'] = 'Forums';
$string['forum:splitdiscussions'] = 'Split discussions';
$string['forum:startdiscussion'] = 'Start new discussions';
@@ -406,6 +409,7 @@ $string['page-mod-forum-discuss'] = 'Forum module discussion thread page';
$string['parent'] = 'Show parent';
$string['parentofthispost'] = 'Parent of this post';
$string['permalink'] = 'Permalink';
$string['postisprivatereply'] = 'This post was made privately and is not visible to all users.';
$string['posttomygroups'] = 'Post a copy to all groups';
$string['posttomygroups_help'] = 'Posts a copy of this message to all groups you have access to. Participants in groups you do not have access to will not see this post';
$string['prevdiscussiona'] = 'Previous discussion: {$a}';
@@ -460,6 +464,7 @@ $string['privacy:metadata:forum_posts:parent'] = 'The parent post that was repli
$string['privacy:metadata:forum_posts:subject'] = 'The subject of the forum post.';
$string['privacy:metadata:forum_posts:totalscore'] = 'The message of the forum post.';
$string['privacy:metadata:forum_posts:userid'] = 'The ID of the user who authored the forum post.';
$string['privacy:metadata:forum_posts:privatereplyto'] = 'The ID of the user this reply was sent to.';
$string['privacy:metadata:forum_queue'] = 'Temporary log of posts that will be mailed in digest form';
$string['privacy:metadata:forum_queue:discussionid'] = 'Forum discussion ID';
$string['privacy:metadata:forum_queue:postid'] = 'Forum post ID';
@@ -487,6 +492,8 @@ $string['privacy:request:delete:discussion:name'] = 'Delete at the request of th
$string['privacy:request:delete:post:message'] = 'The content of this post has been deleted at the request of its author.';
$string['privacy:request:delete:post:subject'] = 'Delete at the request of the author';
$string['privacy:subscribedtoforum'] = 'You are subscribed to this forum.';
$string['privatereply'] = 'Reply privately';
$string['privatereply_help'] = 'A private reply can only be viewed by the author of the post being replied to, and any users with the capability to view private replies.';
$string['processingdigest'] = 'Processing email digest for user {$a}';
$string['processingpost'] = 'Processing post {$a}';
$string['prune'] = 'Split';
+96 -46
View File
@@ -813,6 +813,10 @@ function forum_print_recent_activity($course, $viewfullnames, $timestart) {
}
}
if (!forum_post_is_visible_privately($post, $cm)) {
continue;
}
// Check that the user can see the discussion.
if (forum_is_user_group_discussion($cm, $post->groupid)) {
$printposts[] = $post;
@@ -1038,15 +1042,12 @@ function forum_get_post_full($postid) {
/**
* Gets all posts in discussion including top parent.
*
* @global object
* @global object
* @global object
* @param int $discussionid
* @param string $sort
* @param bool $tracking does user track the forum?
* @return array of posts
* @param int $discussionid The Discussion to fetch.
* @param string $sort The sorting to apply.
* @param bool $tracking Whether the user tracks this forum.
* @return array The posts in the discussion.
*/
function forum_get_all_discussion_posts($discussionid, $sort, $tracking=false) {
function forum_get_all_discussion_posts($discussionid, $sort, $tracking = false) {
global $CFG, $DB, $USER;
$tr_sel = "";
@@ -1525,17 +1526,17 @@ function forum_get_firstpost_from_discussion($discussionid) {
/**
* Returns an array of counts of replies to each discussion
*
* @global object
* @global object
* @param int $forumid
* @param string $forumsort
* @param int $limit
* @param int $page
* @param int $perpage
* @return array
* @param int $forumid
* @param string $forumsort
* @param int $limit
* @param int $page
* @param int $perpage
* @param boolean $canseeprivatereplies Whether the current user can see private replies.
* @return array
*/
function forum_count_discussion_replies($forumid, $forumsort="", $limit=-1, $page=-1, $perpage=0) {
global $CFG, $DB;
function forum_count_discussion_replies($forumid, $forumsort = "", $limit = -1, $page = -1, $perpage = 0,
$canseeprivatereplies = false) {
global $CFG, $DB, $USER;
if ($limit > 0) {
$limitfrom = 0;
@@ -1559,21 +1560,33 @@ function forum_count_discussion_replies($forumid, $forumsort="", $limit=-1, $pag
$groupby = str_replace('asc', '', $groupby);
}
$params = ['forumid' => $forumid];
if (!$canseeprivatereplies) {
$privatewhere = ' AND (p.privatereplyto = :currentuser1 OR p.userid = :currentuser2 OR p.privatereplyto = 0)';
$params['currentuser1'] = $USER->id;
$params['currentuser2'] = $USER->id;
} else {
$privatewhere = '';
}
if (($limitfrom == 0 and $limitnum == 0) or $forumsort == "") {
$sql = "SELECT p.discussion, COUNT(p.id) AS replies, MAX(p.id) AS lastpostid
FROM {forum_posts} p
JOIN {forum_discussions} d ON p.discussion = d.id
WHERE p.parent > 0 AND d.forum = ?
WHERE p.parent > 0 AND d.forum = :forumid
$privatewhere
GROUP BY p.discussion";
return $DB->get_records_sql($sql, array($forumid));
return $DB->get_records_sql($sql, $params);
} else {
$sql = "SELECT p.discussion, (COUNT(p.id) - 1) AS replies, MAX(p.id) AS lastpostid
FROM {forum_posts} p
JOIN {forum_discussions} d ON p.discussion = d.id
WHERE d.forum = ?
WHERE d.forum = :forumid
$privatewhere
GROUP BY p.discussion $groupby $orderby";
return $DB->get_records_sql($sql, array($forumid), $limitfrom, $limitnum);
return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
}
}
@@ -3093,10 +3106,25 @@ function forum_add_new_post($post, $mform, $unused = null) {
$forum = $DB->get_record('forum', array('id' => $discussion->forum));
$cm = get_coursemodule_from_instance('forum', $forum->id);
$context = context_module::instance($cm->id);
$privatereplyto = 0;
// Check whether private replies should be enabled for this post.
if ($post->parent) {
$parent = $DB->get_record('forum_posts', array('id' => $post->parent));
if (!empty($parent->privatereplyto)) {
throw new \coding_exception('It should not be possible to reply to a private reply');
}
if (!empty($post->isprivatereply) && forum_user_can_reply_privately($context, $parent)) {
$privatereplyto = $parent->userid;
}
}
$post->created = $post->modified = time();
$post->mailed = FORUM_MAILED_PENDING;
$post->userid = $USER->id;
$post->privatereplyto = $privatereplyto;
$post->attachment = "";
if (!isset($post->totalscore)) {
$post->totalscore = 0;
@@ -3223,6 +3251,7 @@ function forum_add_discussion($discussion, $mform=null, $unused=null, $userid=nu
$post = new stdClass();
$post->discussion = 0;
$post->parent = 0;
$post->privatereplyto = 0;
$post->userid = $userid;
$post->created = $timenow;
$post->modified = $timenow;
@@ -3449,30 +3478,6 @@ function forum_trigger_content_uploaded_event($post, $cm, $name) {
return true;
}
/**
* @global object
* @param object $post
* @param bool $children
* @return int
*/
function forum_count_replies($post, $children=true) {
global $DB;
$count = 0;
if ($children) {
if ($childposts = $DB->get_records('forum_posts', array('parent' => $post->id))) {
foreach ($childposts as $childpost) {
$count ++; // For this child
$count += forum_count_replies($childpost, true);
}
}
} else {
$count += $DB->count_records('forum_posts', array('parent' => $post->id));
}
return $count;
}
/**
* Given a new post, subscribes or unsubscribes as appropriate.
* Returns some text which describes what happened.
@@ -3989,6 +3994,10 @@ function forum_user_can_see_post($forum, $discussion, $post, $user = null, $cm =
return false;
}
if (!forum_post_is_visible_privately($post, $cm)) {
return false;
}
if (isset($cm->uservisible)) {
if (!$cm->uservisible) {
return false;
@@ -6541,3 +6550,44 @@ function mod_forum_get_completion_active_rule_descriptions($cm) {
}
return $descriptions;
}
/**
* Check whether the forum post is a private reply visible to this user.
*
* @param stdClass $post The post to check.
* @param cm_info $cm The context module instance.
* @return bool Whether the post is visible in terms of private reply configuration.
*/
function forum_post_is_visible_privately($post, $cm) {
global $USER;
if (!empty($post->privatereplyto)) {
// Allow the user to see the private reply if:
// * they hold the permission;
// * they are the author; or
// * they are the intended recipient.
$cansee = false;
$cansee = $cansee || ($post->userid == $USER->id);
$cansee = $cansee || ($post->privatereplyto == $USER->id);
$cansee = $cansee || has_capability('mod/forum:readprivatereplies', context_module::instance($cm->id));
return $cansee;
}
return true;
}
/**
* Check whether the user can reply privately to the parent post.
*
* @param \context_module $context
* @param \stdClass $parent
* @return bool
*/
function forum_user_can_reply_privately(\context_module $context, \stdClass $parent) : bool {
if ($parent->privatereplyto) {
// You cannot reply privately to a post which is, itself, a private reply.
return false;
}
return has_capability('mod/forum:postprivatereply', $context);
}
+330 -255
View File
@@ -50,36 +50,46 @@ $pageparams = array('reply' => $reply, 'forum' => $forum, 'edit' => $edit);
$sitecontext = context_system::instance();
if (!isloggedin() or isguestuser()) {
$entityfactory = mod_forum\local\container::get_entity_factory();
$vaultfactory = mod_forum\local\container::get_vault_factory();
$managerfactory = mod_forum\local\container::get_manager_factory();
$legacydatamapperfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
$urlfactory = mod_forum\local\container::get_url_factory();
$forumvault = $vaultfactory->get_forum_vault();
$forumdatamapper = $legacydatamapperfactory->get_forum_data_mapper();
$discussionvault = $vaultfactory->get_discussion_vault();
$discussiondatamapper = $legacydatamapperfactory->get_discussion_data_mapper();
$postvault = $vaultfactory->get_post_vault();
$postdatamapper = $legacydatamapperfactory->get_post_data_mapper();
if (!isloggedin() or isguestuser()) {
if (!isloggedin() and !get_local_referer()) {
// No referer+not logged in - probably coming in via email See MDL-9052.
require_login();
}
if (!empty($forum)) { // User is starting a new discussion in a forum.
if (! $forum = $DB->get_record('forum', array('id' => $forum))) {
if (!empty($forum)) {
// User is starting a new discussion in a forum.
$forumentity = $forumvault->get_from_id($forum);
if (empty($forumentity)) {
print_error('invalidforumid', 'forum');
}
} else if (!empty($reply)) { // User is writing a new reply.
if (! $parent = forum_get_post_full($reply)) {
} else if (!empty($reply)) {
// User is writing a new reply.
$forumentity = $forumvault->get_from_post_id($reply);
if (empty($forumentity)) {
print_error('invalidparentpostid', 'forum');
}
if (! $discussion = $DB->get_record('forum_discussions', array('id' => $parent->discussion))) {
print_error('notpartofdiscussion', 'forum');
}
if (! $forum = $DB->get_record('forum', array('id' => $discussion->forum))) {
print_error('invalidforumid');
}
}
if (! $course = $DB->get_record('course', array('id' => $forum->course))) {
print_error('invalidcourseid');
}
if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) { // For the logs.
print_error('invalidcoursemodule');
} else {
$modcontext = context_module::instance($cm->id);
$forum = $forumdatamapper->to_legacy_object($forumentity);
$modcontext = $forumentity->get_context();
$course = $forumentity->get_course_record();
if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) {
print_error("invalidcoursemodule");
}
$PAGE->set_cm($cm, $course, $forum);
@@ -96,25 +106,31 @@ if (!isloggedin() or isguestuser()) {
require_login(0, false); // Script is useless unless they're logged in.
$entityfactory = mod_forum\local\container::get_entity_factory();
$vaultfactory = mod_forum\local\container::get_vault_factory();
$canreplyprivately = false;
if (!empty($forum)) { // User is starting a new discussion in a forum.
if (! $forum = $DB->get_record("forum", array("id" => $forum))) {
if (!empty($forum)) {
// User is starting a new discussion in a forum.
$forumentity = $forumvault->get_from_id($forum);
if (empty($forumentity)) {
print_error('invalidforumid', 'forum');
}
if (! $course = $DB->get_record("course", array("id" => $forum->course))) {
print_error('invalidcourseid');
}
if (! $cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) {
$capabilitymanager = $managerfactory->get_capability_manager($forumentity);
$forum = $forumdatamapper->to_legacy_object($forumentity);
$course = $forumentity->get_course_record();
if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) {
print_error("invalidcoursemodule");
}
// Retrieve the contexts.
$modcontext = context_module::instance($cm->id);
$modcontext = $forumentity->get_context();
$coursecontext = context_course::instance($course->id);
if (! forum_user_can_post_discussion($forum, $groupid, -1, $cm)) {
if ($forumentity->is_in_group_mode() && null === $groupid) {
$groupid = groups_get_activity_group($cm);
}
if (!$capabilitymanager->can_create_discussions($USER, $groupid)) {
if (!isguestuser()) {
if (!is_enrolled($coursecontext)) {
if (enrol_selfenrol_available($course->id)) {
@@ -130,7 +146,12 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
}
if (!$cm->visible and !has_capability('moodle/course:viewhiddenactivities', $modcontext)) {
print_error("activityiscurrentlyhidden");
redirect(
$urlfactory->get_course_url_from_forum($forumentity),
get_string('activityiscurrentlyhidden'),
null,
\core\output\notice::NOTIFY_ERROR
);
}
$SESSION->fromurl = get_local_referer(false);
@@ -147,42 +168,45 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
$post->message = '';
$post->messageformat = editors_get_preferred_format();
$post->messagetrust = 0;
if (isset($groupid)) {
$post->groupid = $groupid;
} else {
$post->groupid = groups_get_activity_group($cm);
}
$post->groupid = $groupid;
// Unsetting this will allow the correct return URL to be calculated later.
unset($SESSION->fromdiscussion);
} else if (!empty($reply)) { // User is writing a new reply.
} else if (!empty($reply)) {
// User is writing a new reply.
if (! $parent = forum_get_post_full($reply)) {
$parententity = $postvault->get_from_id($reply);
if (empty($parententity)) {
print_error('invalidparentpostid', 'forum');
}
if (! $discussion = $DB->get_record("forum_discussions", array("id" => $parent->discussion))) {
$discussionentity = $discussionvault->get_from_id($parententity->get_discussion_id());
if (empty($discussionentity)) {
print_error('notpartofdiscussion', 'forum');
}
if (! $forum = $DB->get_record("forum", array("id" => $discussion->forum))) {
$forumentity = $forumvault->get_from_id($discussionentity->get_forum_id());
if (empty($forumentity)) {
print_error('invalidforumid', 'forum');
}
if (! $course = $DB->get_record("course", array("id" => $discussion->course))) {
print_error('invalidcourseid');
}
if (! $cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) {
$capabilitymanager = $managerfactory->get_capability_manager($forumentity);
$parent = $postdatamapper->to_legacy_object($parententity);
$discussion = $discussiondatamapper->to_legacy_object($discussionentity);
$forum = $forumdatamapper->to_legacy_object($forumentity);
$course = $forumentity->get_course_record();
$modcontext = $forumentity->get_context();
$coursecontext = context_course::instance($course->id);
if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) {
print_error('invalidcoursemodule');
}
// Ensure lang, theme, etc. is set up properly. MDL-6926.
$PAGE->set_cm($cm, $course, $forum);
// Retrieve the contexts.
$modcontext = context_module::instance($cm->id);
$coursecontext = context_course::instance($course->id);
if (! forum_user_can_post($forum, $discussion, $USER, $cm, $course, $modcontext)) {
if (!$capabilitymanager->can_reply_to_post($USER, $discussionentity, $parententity)) {
if (!isguestuser()) {
if (!is_enrolled($coursecontext)) { // User is a guest here!
$SESSION->wantsurl = qualified_me();
@@ -215,8 +239,11 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
print_error("activityiscurrentlyhidden");
}
// Load up the $post variable.
if ($parententity->is_private_reply()) {
print_error('cannotreplytoprivatereply', 'forum');
}
// Load up the $post variable.
$post = new stdClass();
$post->course = $course->id;
$post->forum = $forum->id;
@@ -225,6 +252,8 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
$post->subject = $parent->subject;
$post->userid = $USER->id;
$post->message = '';
$post->parentpostauthor = $parent->userid;
$canreplyprivately = $capabilitymanager->can_reply_privately_to_post($USER, $parententity);
$post->groupid = ($discussion->groupid == -1) ? 0 : $discussion->groupid;
@@ -236,30 +265,38 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
// Unsetting this will allow the correct return URL to be calculated later.
unset($SESSION->fromdiscussion);
} else if (!empty($edit)) { // User is editing their own post.
} else if (!empty($edit)) {
// User is editing their own post.
if (! $post = forum_get_post_full($edit)) {
$postentity = $postvault->get_from_id($edit);
if (empty($postentity)) {
print_error('invalidpostid', 'forum');
}
if ($post->parent) {
if (! $parent = forum_get_post_full($post->parent)) {
print_error('invalidparentpostid', 'forum');
}
if ($postentity->has_parent()) {
$parententity = $postvault->get_from_id($postentity->get_parent_id());
$parent = $postdatamapper->to_legacy_object($parententity);
}
if (! $discussion = $DB->get_record("forum_discussions", array("id" => $post->discussion))) {
$discussionentity = $discussionvault->get_from_id($postentity->get_discussion_id());
if (empty($discussionentity)) {
print_error('notpartofdiscussion', 'forum');
}
if (! $forum = $DB->get_record("forum", array("id" => $discussion->forum))) {
$forumentity = $forumvault->get_from_id($discussionentity->get_forum_id());
if (empty($forumentity)) {
print_error('invalidforumid', 'forum');
}
if (! $course = $DB->get_record("course", array("id" => $discussion->course))) {
print_error('invalidcourseid');
}
$capabilitymanager = $managerfactory->get_capability_manager($forumentity);
$post = $postdatamapper->to_legacy_object($postentity);
$discussion = $discussiondatamapper->to_legacy_object($discussionentity);
$forum = $forumdatamapper->to_legacy_object($forumentity);
$course = $forumentity->get_course_record();
$modcontext = $forumentity->get_context();
$coursecontext = context_course::instance($course->id);
if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) {
print_error('invalidcoursemodule');
} else {
$modcontext = context_module::instance($cm->id);
}
$PAGE->set_cm($cm, $course, $forum);
@@ -275,72 +312,92 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
print_error('cannoteditposts', 'forum');
}
// Load up the $post variable.
$post->edit = $edit;
$post->course = $course->id;
$post->forum = $forum->id;
$post->groupid = ($discussion->groupid == -1) ? 0 : $discussion->groupid;
if ($postentity->has_parent()) {
$canreplyprivately = forum_user_can_reply_privately($modcontext, $parent);
}
$post = trusttext_pre_edit($post, 'message', $modcontext);
// Unsetting this will allow the correct return URL to be calculated later.
unset($SESSION->fromdiscussion);
} else if (!empty($delete)) { // User is deleting a post.
} else if (!empty($delete)) {
// User is deleting a post.
if (! $post = forum_get_post_full($delete)) {
$postentity = $postvault->get_from_id($delete);
if (empty($postentity)) {
print_error('invalidpostid', 'forum');
}
if (! $discussion = $DB->get_record("forum_discussions", array("id" => $post->discussion))) {
$discussionentity = $discussionvault->get_from_id($postentity->get_discussion_id());
if (empty($discussionentity)) {
print_error('notpartofdiscussion', 'forum');
}
if (! $forum = $DB->get_record("forum", array("id" => $discussion->forum))) {
$forumentity = $forumvault->get_from_id($discussionentity->get_forum_id());
if (empty($forumentity)) {
print_error('invalidforumid', 'forum');
}
if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $forum->course)) {
$capabilitymanager = $managerfactory->get_capability_manager($forumentity);
$post = $postdatamapper->to_legacy_object($postentity);
$discussion = $discussiondatamapper->to_legacy_object($discussionentity);
$forum = $forumdatamapper->to_legacy_object($forumentity);
$course = $forumentity->get_course_record();
$modcontext = $forumentity->get_context();
$coursecontext = context_course::instance($course->id);
if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) {
print_error('invalidcoursemodule');
}
if (!$course = $DB->get_record('course', array('id' => $forum->course))) {
print_error('invalidcourseid');
}
require_login($course, false, $cm);
$modcontext = context_module::instance($cm->id);
$postentity = $entityfactory->get_post_from_stdclass($post);
$discussionentity = $entityfactory->get_discussion_from_stdclass($discussion);
$forumentity = $entityfactory->get_forum_from_stdclass($forum, $modcontext, $cm, $course);
if ( !(($post->userid == $USER->id && has_capability('mod/forum:deleteownpost', $modcontext))
|| has_capability('mod/forum:deleteanypost', $modcontext)) ) {
print_error('cannotdeletepost', 'forum');
if (!$capabilitymanager->can_delete_post($USER, $discussionentity, $postentity)) {
redirect(
$urlfactory->get_discussion_view_url_from_discussion($discussionentity),
get_string('cannotdeletepost', 'forum'),
null,
\core\output\notice::NOTIFY_ERROR
);
}
$replycount = $postvault->get_reply_count_for_post_id_in_discussion_id(
$USER, $postentity->get_id(), $discussionentity->get_id(), true);
$replycount = forum_count_replies($post);
if (!empty($confirm) && confirm_sesskey()) { // User has confirmed the delete.
if (!empty($confirm) && confirm_sesskey()) {
// User has confirmed the delete.
// Check user capability to delete post.
$timepassed = time() - $post->created;
if (($timepassed > $CFG->maxeditingtime) && !has_capability('mod/forum:deleteanypost', $modcontext)) {
print_error("cannotdeletepost", "forum",
forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $post->discussion))));
}
if ($post->totalscore) {
notice(get_string('couldnotdeleteratings', 'rating'),
forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $post->discussion))));
redirect(
$urlfactory->get_discussion_view_url_from_discussion($discussionentity),
get_string('couldnotdeleteratings', 'rating'),
null,
\core\output\notice::NOTIFY_ERROR
);
} else if ($replycount && !has_capability('mod/forum:deleteanypost', $modcontext)) {
print_error("couldnotdeletereplies", "forum",
forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $post->discussion))));
redirect(
$urlfactory->get_discussion_view_url_from_discussion($discussionentity),
get_string('couldnotdeletereplies', 'rating'),
null,
\core\output\notice::NOTIFY_ERROR
);
} else {
if (! $post->parent) { // Post is a discussion topic as well, so delete discussion.
if (!$postentity->has_parent()) {
// Post is a discussion topic as well, so delete discussion.
if ($forum->type == 'single') {
notice("Sorry, but you are not allowed to delete that discussion!",
forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $post->discussion))));
redirect(
$urlfactory->get_discussion_view_url_from_discussion($discussionentity),
get_string('cannotdeletediscussioninsinglediscussion', 'rating'),
null,
\core\output\notice::NOTIFY_ERROR
);
}
forum_delete_discussion($discussion, false, $course, $cm, $forum);
@@ -356,41 +413,45 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
$event->add_record_snapshot('forum_discussions', $discussion);
$event->trigger();
$message = get_string('eventdiscussiondeleted', 'forum');
redirect(
new moodle_url('/mod/forum/view.php', ['f' => $discussion->forum]),
$message,
$urlfactory->get_forum_view_url_from_forum($forumentity),
get_string('eventdiscussiondeleted', 'forum'),
null,
\core\output\notification::NOTIFY_SUCCESS
);
} else if (forum_delete_post($post, has_capability('mod/forum:deleteanypost', $modcontext),
$course, $cm, $forum)) {
} else {
$deleted = forum_delete_post($post, has_capability('mod/forum:deleteanypost', $modcontext), $course, $cm, $forum);
if (!$deleted) {
redirect(
$urlfactory->get_discussion_view_url_from_post($postentity),
get_string('errorwhiledelete', 'forum'),
null,
\core\output\notice::NOTIFY_ERROR
);
}
if ($forum->type == 'single') {
// Single discussion forums are an exception. We show
// the forum itself since it only has one discussion
// thread.
$discussionurl = new moodle_url("/mod/forum/view.php", array('f' => $forum->id));
// Single discussion forums are an exception.
// We show the forum itself since it only has one discussion thread.
$discussionurl = $urlfactory->get_forum_view_url_from_forum($forumentity);
} else {
$discussionurl = new moodle_url("/mod/forum/discuss.php", array('d' => $discussion->id));
$discussionurl = $urlfactory->get_discussion_view_url_from_discussion($discussionentity);
}
$message = get_string('eventpostdeleted', 'forum');
redirect(
forum_go_back_to($discussionurl),
$message,
get_string('eventpostdeleted', 'forum'),
null,
\core\output\notification::NOTIFY_SUCCESS
);
} else {
print_error('errorwhiledelete', 'forum');
}
}
} else { // User just asked to delete something.
} else {
// User just asked to delete something.
forum_set_return();
$PAGE->navbar->add(get_string('delete', 'forum'));
$PAGE->set_title($course->shortname);
@@ -398,9 +459,14 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
if ($replycount) {
if (!has_capability('mod/forum:deleteanypost', $modcontext)) {
print_error("couldnotdeletereplies", "forum",
forum_go_back_to(new moodle_url('/mod/forum/discuss.php', array('d' => $post->discussion), 'p'.$post->id)));
redirect(
forum_go_back_to($urlfactory->get_view_post_url_from_post($postentity)),
get_string('couldnotdeletereplies', 'rating'),
null,
\core\output\notice::NOTIFY_ERROR
);
}
echo $OUTPUT->header();
echo $OUTPUT->heading(format_string($forum->name), 2);
echo $OUTPUT->confirm(get_string("deletesureplural", "forum", $replycount + 1),
@@ -408,16 +474,21 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
$CFG->wwwroot.'/mod/forum/discuss.php?d='.$post->discussion.'#p'.$post->id);
$postentities = [$postentity];
if (empty($post->edit)) {
$postvault = $vaultfactory->get_post_vault();
$replies = $postvault->get_replies_to_post($postentity, 'created ASC');
$replies = $postvault->get_replies_to_post(
$USER,
$postentity,
// Note: All replies are fetched here as the user has deleteanypost.
true,
'created ASC'
);
$postentities = array_merge($postentities, $replies);
}
$rendererfactory = mod_forum\local\container::get_renderer_factory();
$postsrenderer = $rendererfactory->get_single_discussion_posts_renderer(FORUM_MODE_NESTED, true);
echo $postsrenderer->render($USER, [$forumentity], [$discussionentity], [$postentity]);
echo $postsrenderer->render($USER, [$forumentity], [$discussionentity], $postentities);
} else {
echo $OUTPUT->header();
echo $OUTPUT->heading(format_string($forum->name), 2);
@@ -434,31 +505,51 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
echo $OUTPUT->footer();
die;
} else if (!empty($prune)) {
// Pruning.
} else if (!empty($prune)) { // Pruning.
if (!$post = forum_get_post_full($prune)) {
$postentity = $postvault->get_from_id($prune);
if (empty($postentity)) {
print_error('invalidpostid', 'forum');
}
if (!$discussion = $DB->get_record("forum_discussions", array("id" => $post->discussion))) {
$discussionentity = $discussionvault->get_from_id($postentity->get_discussion_id());
if (empty($discussionentity)) {
print_error('notpartofdiscussion', 'forum');
}
if (!$forum = $DB->get_record("forum", array("id" => $discussion->forum))) {
$forumentity = $forumvault->get_from_id($discussionentity->get_forum_id());
if (empty($forumentity)) {
print_error('invalidforumid', 'forum');
}
if ($forum->type == 'single') {
print_error('cannotsplit', 'forum');
}
if (!$post->parent) {
print_error('alreadyfirstpost', 'forum');
}
if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $forum->course)) { // For the logs.
$capabilitymanager = $managerfactory->get_capability_manager($forumentity);
$post = $postdatamapper->to_legacy_object($postentity);
$discussion = $discussiondatamapper->to_legacy_object($discussionentity);
$forum = $forumdatamapper->to_legacy_object($forumentity);
$course = $forumentity->get_course_record();
$modcontext = $forumentity->get_context();
$coursecontext = context_course::instance($course->id);
if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) {
print_error('invalidcoursemodule');
} else {
$modcontext = context_module::instance($cm->id);
}
if (!has_capability('mod/forum:splitdiscussions', $modcontext)) {
print_error('cannotsplit', 'forum');
if (!$postentity->has_parent()) {
redirect(
$urlfactory->get_discussion_view_url_from_discussion($discussionentity),
get_string('alreadyfirstpost', 'forum'),
null,
\core\output\notification::NOTIFY_ERROR
);
}
if (!$capabilitymanager->can_split_post($USER, $discussionentity, $postentity)) {
redirect(
$urlfactory->get_discussion_view_url_from_discussion($discussionentity),
get_string('cannotsplit', 'forum'),
null,
\core\output\notification::NOTIFY_ERROR
);
}
$PAGE->set_cm($cm);
@@ -467,7 +558,7 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
$prunemform = new mod_forum_prune_form(null, array('prune' => $prune, 'confirm' => $prune));
if ($prunemform->is_cancelled()) {
redirect(forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $post->discussion))));
redirect(forum_go_back_to($urlfactory->get_discussion_view_url_from_discussion($discussionentity)));
} else if ($fromform = $prunemform->get_data()) {
// User submits the data.
$newdiscussion = new stdClass();
@@ -490,6 +581,7 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
$newpost->subject = $name;
$DB->update_record("forum_posts", $newpost);
$postentity = $postvault->get_from_id($postentity->get_id());
forum_change_discussionid($post->id, $newid);
@@ -531,10 +623,9 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
$event->add_record_snapshot('forum_discussions', $discussion);
$event->trigger();
$message = get_string('discussionsplit', 'forum');
redirect(
forum_go_back_to(new moodle_url("/mod/forum/discuss.php", array('d' => $newid))),
$message,
forum_go_back_to($urlfactory->get_discussion_view_url_from_post($postentity)),
get_string('discussionsplit', 'forum'),
null,
\core\output\notification::NOTIFY_SUCCESS
);
@@ -567,18 +658,8 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
}
if (!isset($coursecontext)) {
// Has not yet been set by post.php.
$coursecontext = context_course::instance($forum->course);
}
// From now on user must be logged on properly.
if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id)) { // For the logs.
print_error('invalidcoursemodule');
}
$modcontext = context_module::instance($cm->id);
require_login($course, false, $cm);
if (isguestuser()) {
@@ -586,21 +667,19 @@ if (isguestuser()) {
print_error('noguest');
}
if (!isset($forum->maxattachments)) { // TODO - delete this once we add a field to the forum table.
$forum->maxattachments = 3;
}
$thresholdwarning = forum_check_throttling($forum, $cm);
$mformpost = new mod_forum_post_form('post.php', array('course' => $course,
'cm' => $cm,
'coursecontext' => $coursecontext,
'modcontext' => $modcontext,
'forum' => $forum,
'post' => $post,
'subscribe' => \mod_forum\subscriptions::is_subscribed($USER->id, $forum,
null, $cm),
'thresholdwarning' => $thresholdwarning,
'edit' => $edit), 'post', '', array('id' => 'mformforum'));
$mformpost = new mod_forum_post_form('post.php', [
'course' => $course,
'cm' => $cm,
'coursecontext' => $coursecontext,
'modcontext' => $modcontext,
'forum' => $forum,
'post' => $post,
'subscribe' => \mod_forum\subscriptions::is_subscribed($USER->id, $forum, null, $cm),
'thresholdwarning' => $thresholdwarning,
'edit' => $edit,
'canreplyprivately' => $canreplyprivately,
], 'post', '', array('id' => 'mformforum'));
$draftitemid = file_get_submitted_draft_itemid('attachments');
$postid = empty($post->id) ? null : $post->id;
@@ -694,16 +773,16 @@ $mformpost->set_data(
);
if ($mformpost->is_cancelled()) {
if (!isset($discussion->id) || $forum->type === 'qanda') {
// Q and A forums don't have a discussion page, so treat them like a new thread..
redirect(new moodle_url('/mod/forum/view.php', array('f' => $forum->id)));
if (!isset($discussion->id) || $forum->type === 'single') {
// Single forums don't have a discussion page.
redirect($urlfactory->get_forum_view_url_from_forum($forumentity));
} else {
redirect(new moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id)));
redirect($urlfactory->get_discussion_view_url_from_discussion($discussionentity));
}
} else if ($fromform = $mformpost->get_data()) {
if (empty($SESSION->fromurl)) {
$errordestination = "$CFG->wwwroot/mod/forum/view.php?f=$forum->id";
$errordestination = $urlfactory->get_forum_view_url_from_forum($forumentity);
} else {
$errordestination = $SESSION->fromurl;
}
@@ -717,43 +796,44 @@ if ($mformpost->is_cancelled()) {
// Clean message text.
$fromform = trusttext_pre_edit($fromform, 'message', $modcontext);
if ($fromform->edit) { // Updating a post.
if ($fromform->edit) {
// Updating a post.
unset($fromform->groupid);
$fromform->id = $fromform->edit;
$message = '';
// Fix for bug #4314.
if (!$realpost = $DB->get_record('forum_posts', array('id' => $fromform->id))) {
$realpost = new stdClass();
$realpost->userid = -1;
if (!$capabilitymanager->can_edit_post($USER, $discussionentity, $postentity)) {
redirect(
$urlfactory->get_view_post_url_from_post($postentity),
get_string('cannotupdatepost', 'forum'),
null,
\core\output\notification::ERROR
);
}
// If user has edit any post capability
// or has either startnewdiscussion or reply capability and is editting own post
// then he can proceed
// MDL-7066.
if ( !(($realpost->userid == $USER->id && (has_capability('mod/forum:replypost', $modcontext)
|| has_capability('mod/forum:startdiscussion', $modcontext))) ||
has_capability('mod/forum:editanypost', $modcontext)) ) {
print_error('cannotupdatepost', 'forum');
}
// If the user has access to all groups and they are changing the group, then update the post.
if (isset($fromform->groupinfo) && has_capability('mod/forum:movediscussions', $modcontext)) {
if (isset($fromform->groupinfo) && $capabilitymanager->can_move_discussions($USER)) {
// If the user has access to all groups and they are changing the group, then update the post.
if (empty($fromform->groupinfo)) {
$fromform->groupinfo = -1;
}
if (!forum_user_can_post_discussion($forum, $fromform->groupinfo, null, $cm, $modcontext)) {
print_error('cannotupdatepost', 'forum');
if (!$capabilitymanager->can_create_discussions($USER, $fromform->groupinfo)) {
redirect(
$urlfactory->get_view_post_url_from_post($postentity),
get_string('cannotupdatepost', 'forum'),
null,
\core\output\notification::ERROR
);
}
$DB->set_field('forum_discussions', 'groupid', $fromform->groupinfo, array('firstpost' => $fromform->id));
if ($discussionentity->get_group_id() != $fromform->groupinfo) {
$DB->set_field('forum_discussions', 'groupid', $fromform->groupinfo, array('firstpost' => $fromform->id));
}
}
// When editing first post/discussion.
if (!$fromform->parent) {
if (has_capability('mod/forum:pindiscussions', $modcontext)) {
if ($postentity->has_parent()) {
if ($capabilitymanager->can_pin_discussions($USER)) {
// Can change pinned if we have capability.
$fromform->pinned = !empty($fromform->pinned) ? FORUM_DISCUSSION_PINNED : FORUM_DISCUSSION_UNPINNED;
} else {
@@ -761,35 +841,33 @@ if ($mformpost->is_cancelled()) {
unset($fromform->pinned);
}
}
$updatepost = $fromform; // Realpost.
$updatepost = $fromform;
$updatepost->forum = $forum->id;
if (!forum_update_post($updatepost, $mformpost)) {
print_error("couldnotupdate", "forum", $errordestination);
}
// MDL-11818.
if (($forum->type == 'single') && ($updatepost->parent == '0')) {
if ('single' == $forumentity->get_type() && !$postentity->has_parent()) {
// Updating first post of single discussion type -> updating forum intro.
$forum->intro = $updatepost->message;
$forum->timemodified = time();
$DB->update_record("forum", $forum);
}
if ($realpost->userid == $USER->id) {
if ($USER->id === $postentity->get_author_id()) {
$message .= get_string("postupdated", "forum");
} else {
$realuser = $DB->get_record('user', array('id' => $realpost->userid));
$realuser = \core_user::get_user($postentity->get_author_id());
$message .= get_string("editedpostupdated", "forum", fullname($realuser));
}
$subscribemessage = forum_post_subscription($fromform, $forum, $discussion);
if ($forum->type == 'single') {
// Single discussion forums are an exception. We show
// the forum itself since it only has one discussion
// thread.
$discussionurl = new moodle_url("/mod/forum/view.php", array('f' => $forum->id));
if ('single' == $forumentity->get_type()) {
// Single discussion forums are an exception.
// We show the forum itself since it only has one discussion thread.
$discussionurl = $urlfactory->get_forum_view_url_from_forum($forumentity);
} else {
$discussionurl = new moodle_url("/mod/forum/discuss.php", array('d' => $discussion->id), 'p' . $fromform->id);
$discussionurl = $urlfactory->get_view_post_url_from_post($postentity);
}
$params = array(
@@ -802,8 +880,8 @@ if ($mformpost->is_cancelled()) {
)
);
if ($realpost->userid !== $USER->id) {
$params['relateduserid'] = $realpost->userid;
if ($USER->id !== $postentity->get_author_id()) {
$params['relateduserid'] = $postentity->get_author_id();
}
$event = \mod_forum\event\post_updated::create($params);
@@ -817,7 +895,8 @@ if ($mformpost->is_cancelled()) {
\core\output\notification::NOTIFY_SUCCESS
);
} else if ($fromform->discussion) { // Adding a new post to an existing discussion
} else if ($fromform->discussion) {
// Adding a new post to an existing discussion
// Before we add this we must check that the user will not exceed the blocking threshold.
forum_check_blocking_threshold($thresholdwarning);
@@ -826,6 +905,7 @@ if ($mformpost->is_cancelled()) {
$addpost = $fromform;
$addpost->forum = $forum->id;
if ($fromform->id = forum_add_new_post($addpost, $mformpost)) {
$postentity = $postvault->get_from_id($fromform->id);
$fromform->deleted = 0;
$subscribemessage = forum_post_subscription($fromform, $forum, $discussion);
@@ -837,12 +917,11 @@ if ($mformpost->is_cancelled()) {
}
if ($forum->type == 'single') {
// Single discussion forums are an exception. We show
// the forum itself since it only has one discussion
// thread.
$discussionurl = new moodle_url("/mod/forum/view.php", array('f' => $forum->id), 'p'.$fromform->id);
// Single discussion forums are an exception.
// We show the forum itself since it only has one discussion thread.
$discussionurl = $urlfactory->get_forum_view_url_from_forum($forumentity);
} else {
$discussionurl = new moodle_url("/mod/forum/discuss.php", array('d' => $discussion->id), 'p'.$fromform->id);
$discussionurl = $urlfactory->get_view_post_url_from_post($postentity);
}
$params = array(
@@ -878,7 +957,8 @@ if ($mformpost->is_cancelled()) {
}
exit;
} else { // Adding a new discussion.
} else {
// Adding a new discussion.
// The location to redirect to after successfully posting.
$redirectto = new moodle_url('/mod/forum/view.php', array('f' => $fromform->forum));
@@ -893,11 +973,10 @@ if ($mformpost->is_cancelled()) {
}
$discussion->timestart = $fromform->timestart;
$discussion->timeend = $fromform->timeend;
$discussion->pinned = FORUM_DISCUSSION_UNPINNED;
if (has_capability('mod/forum:pindiscussions', $modcontext) && !empty($fromform->pinned)) {
if (!empty($fromform->pinned) && $capabilitymanager->can_pin_discussions($USER)) {
$discussion->pinned = FORUM_DISCUSSION_PINNED;
} else {
$discussion->pinned = FORUM_DISCUSSION_UNPINNED;
}
$allowedgroups = array();
@@ -912,7 +991,7 @@ if ($mformpost->is_cancelled()) {
// Note: all groups are returned when in visible groups mode so we must manually filter.
$allowedgroups = groups_get_activity_allowed_groups($cm);
foreach ($allowedgroups as $groupid => $group) {
if (forum_user_can_post_discussion($forum, $groupid, -1, $cm, $modcontext)) {
if ($capabilitymanager->can_create_discussions($USER, $groupid)) {
$groupstopostto[] = $groupid;
}
}
@@ -933,7 +1012,7 @@ if ($mformpost->is_cancelled()) {
forum_check_blocking_threshold($thresholdwarning);
foreach ($groupstopostto as $group) {
if (!forum_user_can_post_discussion($forum, $group, -1, $cm, $modcontext)) {
if (!$capabilitymanager->can_create_discussions($USER, $groupid)) {
print_error('cannotcreatediscussion', 'forum');
}
@@ -983,21 +1062,15 @@ if ($mformpost->is_cancelled()) {
}
// This section is only shown after all checks are in place, and the forumentity and any relevant discussion and post
// entity are available.
// To get here they need to edit a post, and the $post
// variable will be loaded with all the particulars,
// so bring up the form.
// Vars $course, $forum are defined. $discussion is for edit and reply only.
if ($post->discussion) {
if (! $toppost = $DB->get_record("forum_posts", array("discussion" => $post->discussion, "parent" => 0))) {
print_error('cannotfindparentpost', 'forum', '', $post->id);
}
if (!empty($discussionentity)) {
$titlesubject = format_string($discussionentity->get_name(), true);
} else if ('news' == $forumentity->get_type()) {
$titlesubject = get_string("addanewtopic", "forum");
} else {
$toppost = new stdClass();
$toppost->subject = ($forum->type == "news") ? get_string("addanewtopic", "forum") :
get_string("addanewdiscussion", "forum");
$titlesubject = get_string("addanewdiscussion", "forum");
}
if (empty($post->edit)) {
@@ -1010,20 +1083,22 @@ if (empty($discussion->name)) {
}
$discussion->name = $forum->name;
}
if ($forum->type == 'single') {
$strdiscussionname = '';
if ('single' == $forumentity->get_type()) {
// There is only one discussion thread for this forum type. We should
// not show the discussion name (same as forum name in this case) in
// the breadcrumbs.
$strdiscussionname = '';
} else {
} else if (!empty($discussionentity)) {
// Show the discussion name in the breadcrumbs.
$strdiscussionname = format_string($discussion->name).':';
$strdiscussionname = format_string($discussionentity->get_name()) . ': ';
}
$forcefocus = empty($reply) ? null : 'message';
if (!empty($discussion->id)) {
$PAGE->navbar->add(format_string($toppost->subject, true), "discuss.php?d=$discussion->id");
$PAGE->navbar->add($titlesubject, $urlfactory->get_discussion_view_url_from_discussion($discussionentity));
}
if ($post->parent) {
@@ -1034,25 +1109,27 @@ if ($edit) {
$PAGE->navbar->add(get_string('edit', 'forum'));
}
$PAGE->set_title("$course->shortname: $strdiscussionname ".format_string($toppost->subject));
$PAGE->set_title("{$course->shortname}: {$strdiscussionname}{$titlesubject}");
$PAGE->set_heading($course->fullname);
echo $OUTPUT->header();
echo $OUTPUT->heading(format_string($forum->name), 2);
// Checkup.
if (!empty($parent) && !forum_user_can_see_post($forum, $discussion, $post, null, $cm)) {
if (!empty($parententity) && !$capabilitymanager->can_view_post($USER, $discussionentity, $parententity)) {
print_error('cannotreply', 'forum');
}
if (empty($parent) && empty($edit) && !forum_user_can_post_discussion($forum, $groupid, -1, $cm, $modcontext)) {
if (empty($parententity) && empty($edit) && !$capabilitymanager->can_create_discussions($USER, $groupid)) {
print_error('cannotcreatediscussion', 'forum');
}
if ($forum->type == 'qanda'
&& !has_capability('mod/forum:viewqandawithoutposting', $modcontext)
&& !empty($discussion->id)
&& !forum_user_has_posted($forum->id, $discussion->id, $USER->id)) {
echo $OUTPUT->notification(get_string('qandanotify', 'forum'));
if (!empty($discussionentity) && 'qanda' == $forumentity->get_type()) {
$displaywarning = $capabilitymanager->must_post_before_viewing_discussion($USER, $discussionentity);
$displaywarning = $displaywarning && !forum_user_has_posted($forumentity->get_id(), $discussionentity->get_id(), $USER->id);
if ($displaywarning) {
echo $OUTPUT->notification(get_string('qandanotify', 'forum'));
}
}
// If there is a warning message and we are not editing a post we need to handle the warning.
@@ -1061,27 +1138,24 @@ if (!empty($thresholdwarning) && !$edit) {
forum_check_blocking_threshold($thresholdwarning);
}
if (!empty($parent)) {
if (!$discussion = $DB->get_record('forum_discussions', array('id' => $parent->discussion))) {
print_error('notpartofdiscussion', 'forum');
}
$postentity = $entityfactory->get_post_from_stdclass($parent);
$discussionentity = $entityfactory->get_discussion_from_stdclass($discussion);
$forumentity = $entityfactory->get_forum_from_stdclass($forum, $modcontext, $cm, $course);
$postentities = [$postentity];
if (!empty($parententity)) {
$postentities = [$parententity];
if (empty($post->edit)) {
if ($forum->type != 'qanda' || forum_user_can_see_discussion($forum, $discussion, $modcontext)) {
$postvault = $vaultfactory->get_post_vault();
$replies = $postvault->get_replies_to_post($postentity, 'created ASC');
if ('qanda' != $forumentity->get_type() || forum_user_can_see_discussion($forum, $discussion, $modcontext)) {
$replies = $postvault->get_replies_to_post(
$USER,
$parententity,
$capabilitymanager->can_view_any_private_reply($USER),
'created ASC'
);
$postentities = array_merge($postentities, $replies);
}
}
$rendererfactory = mod_forum\local\container::get_renderer_factory();
$postsrenderer = $rendererfactory->get_single_discussion_posts_renderer(FORUM_MODE_THREADED, true);
echo $postsrenderer->render($USER, [$forumentity], [$discussionentity], [$postentity]);
echo $postsrenderer->render($USER, [$forumentity], [$discussionentity], $postentities);
} else {
if (!empty($forum->intro)) {
echo $OUTPUT->box(format_module_intro('forum', $forum, $cm->id), 'generalbox', 'intro');
@@ -1098,9 +1172,10 @@ if (!empty($formheading)) {
echo $OUTPUT->heading($formheading, 2, array('class' => 'accesshide'));
}
$data = new StdClass();
if (isset($postid)) {
$data->tags = core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $postid);
if (!empty($postentity)) {
$data = (object) [
'tags' => core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $postentity->get_id())
];
$mformpost->set_data($data);
}
+12
View File
@@ -204,6 +204,8 @@ function forum_rss_feed_discussions_sql($forum, $cm, $newsince=0) {
* @return string the SQL query to be used to get the Post details from the forum table of the database
*/
function forum_rss_feed_posts_sql($forum, $cm, $newsince=0) {
global $USER;
$modcontext = context_module::instance($cm->id);
// Get group enforcement SQL.
@@ -224,6 +226,15 @@ function forum_rss_feed_posts_sql($forum, $cm, $newsince=0) {
$newsince = '';
}
$canseeprivatereplies = has_capability('mod/forum:readprivatereplies', $modcontext);
if (!$canseeprivatereplies) {
$privatewhere = ' AND (p.privatereplyto = :currentuser1 OR p.userid = :currentuser2 OR p.privatereplyto = 0)';
$params['currentuser1'] = $USER->id;
$params['currentuser2'] = $USER->id;
} else {
$privatewhere = '';
}
$usernamefields = get_all_user_name_fields(true, 'u');
$sql = "SELECT p.id AS postid,
d.id AS discussionid,
@@ -245,6 +256,7 @@ function forum_rss_feed_posts_sql($forum, $cm, $newsince=0) {
WHERE d.forum = {$forum->id} AND
p.discussion = d.id AND p.deleted <> 1 AND
u.id = p.userid $newsince
$privatewhere
$groupselect
ORDER BY p.created desc";
+4
View File
@@ -327,3 +327,7 @@ span.unread {
.path-mod-forum article .nav .nav-link + .nav-link {
border-left: 1px solid #ddd;
}
.privatereplyinfo {
font-size: 80%;
}
@@ -58,6 +58,11 @@
{{{html.authorsubheading}}}
</address>
{{/isdeleted}}
{{#isprivatereply}}
<div class="privatereplyinfo">
{{#str}}postisprivatereply, forum{{/str}}
</div>
{{/isprivatereply}}
</header>
<div>
{{{message}}}
@@ -49,6 +49,7 @@
* permalink
* unsubscribeforumlink
* unsubscribediscussionlink
* isprivatereply
Example context (json):
{
@@ -92,6 +93,11 @@
"date": {{# quote }}{{ postdate }}{{/ quote }}
} {{/ str }}
</div>
{{# isprivatereply }}
<div class="privatereplyinfo">
{{# str }} postisprivatereply, forum {{/ str }}
</div>
{{/ isprivatereply }}
</td>
</tr>
<tr>
@@ -41,6 +41,7 @@
* unsubscribeforumlink
* unsubscribediscussionlink
* forumindexlink
* isprivatereply
}}
{{{ coursename }}} -> {{# str }} forums, forum {{/ str }} -> {{{ forumname }}}{{# showdiscussionname }} -> {{{ discussionname }}} {{/ showdiscussionname }}
{{ permalink }}
@@ -49,6 +50,9 @@
"name": {{# quote }}{{{ authorfullname }}}{{/ quote }},
"date": {{# quote}}{{ postdate }}{{/ quote }}
} {{/ str }}
{{# isprivatereply }}
{{# str }} postisprivatereply, forum {{/ str }}
{{/ isprivatereply }}
---------------------------------------------------------------------
{{{ message }}}
@@ -0,0 +1,64 @@
@mod @mod_forum
Feature: Forum posts can be replied to in private
In order to post feedback to my students
As a Teacher
I need to be able to reply privately to students
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| teacher2 | Teacher | 2 | teacher2@example.com |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Science 101 | C1 | 0 |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| forum | Study discussions | Test forum description | C1 | forum |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| teacher2 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
And I log in as "student1"
And I am on "Science 101" course homepage
And I add a new discussion to "Study discussions" forum with:
| Subject | Answers to the homework |
| Message | Here are the answers to last night's homework. |
And I log out
And I log in as "teacher1"
And I am on "Science 101" course homepage
And I reply "Answers to the homework" post from "Study discussions" forum with:
| Message | How about you and I have a meeting after class about plagiarism? |
| Reply privately | 1 |
Scenario: As a teacher I can see my own response
Given I follow "Answers to the homework"
Then I should see "How about you and I have a meeting after class about plagiarism?"
Scenario: As a fellow teacher I can see the other teacher's response
Given I log out
And I log in as "teacher2"
And I am on "Science 101" course homepage
And I follow "Study discussions"
When I follow "Answers to the homework"
Then I should see "How about you and I have a meeting after class about plagiarism?"
Scenario: As the intended recipient I can see my own response
Given I log out
And I log in as "student1"
And I am on "Science 101" course homepage
And I follow "Study discussions"
When I follow "Answers to the homework"
Then I should see "How about you and I have a meeting after class about plagiarism?"
Scenario: As a non-privileged user I cannot see my own response
Given I log out
And I log in as "student2"
And I am on "Science 101" course homepage
And I follow "Study discussions"
When I follow "Answers to the homework"
Then I should not see "How about you and I have a meeting after class about plagiarism?"
+1
View File
@@ -35,6 +35,7 @@ return new class extends phpunit_coverage_info {
/** @var array The list of folders relative to the plugin root to whitelist in coverage generation. */
protected $whitelistfolders = [
'classes/local',
'externallib.php',
];
/** @var array The list of files relative to the plugin root to whitelist in coverage generation. */
@@ -89,6 +89,7 @@ class mod_forum_entities_discussion_summary_testcase extends advanced_testcase {
false,
0,
false,
false,
false
);
@@ -73,6 +73,7 @@ class mod_forum_entities_discussion_testcase extends advanced_testcase {
false,
0,
false,
false,
false
);
$notfirstpost = new post_entity(
@@ -90,6 +91,7 @@ class mod_forum_entities_discussion_testcase extends advanced_testcase {
false,
0,
false,
false,
false
);
@@ -57,6 +57,7 @@ class mod_forum_entities_post_read_receipt_collection_testcase extends advanced_
false,
0,
false,
false,
false
);
$post = new post_entity(
@@ -74,6 +75,7 @@ class mod_forum_entities_post_read_receipt_collection_testcase extends advanced_
false,
0,
false,
false,
false
);
$collection = new collection_entity([
+1
View File
@@ -58,6 +58,7 @@ class mod_forum_entities_post_testcase extends advanced_testcase {
false,
0,
false,
false,
false
);
+398
View File
@@ -310,6 +310,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
'userfullname' => fullname($user3),
'userpictureurl' => '',
'deleted' => false,
'isprivatereply' => false,
);
$expectedposts['posts'][] = array(
@@ -346,6 +347,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
'userfullname' => fullname($user2),
'userpictureurl' => '',
'deleted' => false,
'isprivatereply' => false,
);
// Test a discussion with two additional posts (total 3 posts).
@@ -418,6 +420,308 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
}
}
/**
* Test get forum posts
*
* Tests is similar to the get_forum_discussion_posts only utilizing the new return structure and entities
*/
public function test_mod_forum_get_discussion_posts() {
global $CFG, $PAGE;
$this->resetAfterTest(true);
// Set the CFG variable to allow track forums.
$CFG->forum_trackreadposts = true;
$urlfactory = mod_forum\local\container::get_url_factory();
$legacyfactory = mod_forum\local\container::get_legacy_data_mapper_factory();
$entityfactory = mod_forum\local\container::get_entity_factory();
// Create a user who can track forums.
$record = new stdClass();
$record->trackforums = true;
$user1 = self::getDataGenerator()->create_user($record);
// Create a bunch of other users to post.
$user2 = self::getDataGenerator()->create_user();
$user2entity = $entityfactory->get_author_from_stdclass($user2);
$exporteduser2 = [
'id' => (int) $user2->id,
'fullname' => fullname($user2),
'groups' => [],
'urls' => [
'profile' => $urlfactory->get_author_profile_url($user2entity),
'profileimage' => $urlfactory->get_author_profile_image_url($user2entity),
]
];
$user2->fullname = $exporteduser2['fullname'];
$user3 = self::getDataGenerator()->create_user(['fullname' => "Mr Pants 1"]);
$user3entity = $entityfactory->get_author_from_stdclass($user3);
$exporteduser3 = [
'id' => (int) $user3->id,
'fullname' => fullname($user3),
'groups' => [],
'urls' => [
'profile' => $urlfactory->get_author_profile_url($user3entity),
'profileimage' => $urlfactory->get_author_profile_image_url($user3entity),
]
];
$user3->fullname = $exporteduser3['fullname'];
$forumgenerator = self::getDataGenerator()->get_plugin_generator('mod_forum');
// Set the first created user to the test user.
self::setUser($user1);
// Create course to add the module.
$course1 = self::getDataGenerator()->create_course();
// Forum with tracking off.
$record = new stdClass();
$record->course = $course1->id;
$record->trackingtype = FORUM_TRACKING_OFF;
$forum1 = self::getDataGenerator()->create_module('forum', $record);
$forum1context = context_module::instance($forum1->cmid);
// Forum with tracking enabled.
$record = new stdClass();
$record->course = $course1->id;
$forum2 = self::getDataGenerator()->create_module('forum', $record);
$forum2cm = get_coursemodule_from_id('forum', $forum2->cmid);
$forum2context = context_module::instance($forum2->cmid);
// Add discussions to the forums.
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user1->id;
$record->forum = $forum1->id;
$discussion1 = $forumgenerator->create_discussion($record);
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user2->id;
$record->forum = $forum1->id;
$discussion2 = $forumgenerator->create_discussion($record);
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user2->id;
$record->forum = $forum2->id;
$discussion3 = $forumgenerator->create_discussion($record);
// Add 2 replies to the discussion 1 from different users.
$record = new stdClass();
$record->discussion = $discussion1->id;
$record->parent = $discussion1->firstpost;
$record->userid = $user2->id;
$discussion1reply1 = $forumgenerator->create_post($record);
$filename = 'shouldbeanimage.jpg';
// Add a fake inline image to the post.
$filerecordinline = array(
'contextid' => $forum1context->id,
'component' => 'mod_forum',
'filearea' => 'post',
'itemid' => $discussion1reply1->id,
'filepath' => '/',
'filename' => $filename,
);
$fs = get_file_storage();
$timepost = time();
$fs->create_file_from_string($filerecordinline, 'image contents (not really)');
$record->parent = $discussion1reply1->id;
$record->userid = $user3->id;
$discussion1reply2 = $forumgenerator->create_post($record);
// Enrol the user in the course.
$enrol = enrol_get_plugin('manual');
// Following line enrol and assign default role id to the user.
// So the user automatically gets mod/forum:viewdiscussion on all forums of the course.
$this->getDataGenerator()->enrol_user($user1->id, $course1->id);
$this->getDataGenerator()->enrol_user($user2->id, $course1->id);
// Delete one user, to test that we still receive posts by this user.
delete_user($user3);
// Create what we expect to be returned when querying the discussion.
$expectedposts = array(
'posts' => array(),
'ratinginfo' => array(
'contextid' => $forum1context->id,
'component' => 'mod_forum',
'ratingarea' => 'post',
'canviewall' => null,
'canviewany' => null,
'scales' => array(),
'ratings' => array(),
),
'warnings' => array(),
);
// User pictures are initially empty, we should get the links once the external function is called.
$isolatedurl = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply2->discussion);
$isolatedurl->params(['parent' => $discussion1reply2->id]);
$expectedposts['posts'][] = array(
'id' => $discussion1reply2->id,
'discussionid' => $discussion1reply2->discussion,
'parentid' => $discussion1reply2->parent,
'hasparent' => true,
'timecreated' => $discussion1reply2->created,
'subject' => $discussion1reply2->subject,
'message' => file_rewrite_pluginfile_urls($discussion1reply2->message, 'pluginfile.php',
$forum1context->id, 'mod_forum', 'post', $discussion1reply2->id),
'messageformat' => 1, // This value is usually changed by external_format_text() function.
'unread' => null,
'isdeleted' => false,
'isprivatereply' => false,
'haswordcount' => false,
'wordcount' => null,
'author'=> $exporteduser3,
'attachments' => [],
'tags' => [],
'html' => [
'rating' => null,
'taglist' => null,
'authorsubheading' => $forumgenerator->get_author_subheading_html((object)$exporteduser3, $discussion1reply2->created)
],
'capabilities' => [
'view' => 1,
'edit' => 0,
'delete' => 0,
'split' => 0,
'reply' => 1,
'export' => 0,
'controlreadstatus' => 0
],
'urls' => [
'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply2->discussion, $discussion1reply2->id),
'viewisolated' => $isolatedurl->out(false),
'viewparent' => $urlfactory->get_view_post_url_from_post_id($discussion1reply2->discussion, $discussion1reply2->parent),
'edit' => null,
'delete' =>null,
'split' => null,
'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
'reply' => $discussion1reply2->id
]))->out(false),
'export' => null,
'markasread' => null,
'markasunread' => null,
'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply2->discussion),
],
);
$isolatedurl = $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion);
$isolatedurl->params(['parent' => $discussion1reply1->id]);
$expectedposts['posts'][] = array(
'id' => $discussion1reply1->id,
'discussionid' => $discussion1reply1->discussion,
'parentid' => $discussion1reply1->parent,
'hasparent' => true,
'timecreated' => $discussion1reply1->created,
'subject' => $discussion1reply1->subject,
'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
$forum1context->id, 'mod_forum', 'post', $discussion1reply1->id),
'messageformat' => 1, // This value is usually changed by external_format_text() function.
'unread' => null,
'isdeleted' => false,
'isprivatereply' => false,
'haswordcount' => false,
'wordcount' => null,
'author'=> $exporteduser2,
'attachments' => [],
'tags' => [],
'html' => [
'rating' => null,
'taglist' => null,
'authorsubheading' => $forumgenerator->get_author_subheading_html((object)$exporteduser2, $discussion1reply1->created)
],
'capabilities' => [
'view' => 1,
'edit' => 0,
'delete' => 0,
'split' => 0,
'reply' => 1,
'export' => 0,
'controlreadstatus' => 0
],
'urls' => [
'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply1->discussion, $discussion1reply1->id),
'viewisolated' => $isolatedurl->out(false),
'viewparent' => $urlfactory->get_view_post_url_from_post_id($discussion1reply1->discussion, $discussion1reply1->parent),
'edit' => null,
'delete' =>null,
'split' => null,
'reply' => (new moodle_url('/mod/forum/post.php#mformforum', [
'reply' => $discussion1reply1->id
]))->out(false),
'export' => null,
'markasread' => null,
'markasunread' => null,
'discuss' => $urlfactory->get_discussion_view_url_from_discussion_id($discussion1reply1->discussion),
],
);
// Test a discussion with two additional posts (total 3 posts).
$posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC');
$posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
$this->assertEquals(3, count($posts['posts']));
// Unset the initial discussion post.
array_pop($posts['posts']);
$this->assertEquals($expectedposts, $posts);
// Check we receive the unread count correctly on tracked forum.
forum_tp_count_forum_unread_posts($forum2cm, $course1, true); // Reset static cache.
$result = mod_forum_external::get_forums_by_courses(array($course1->id));
$result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
foreach ($result as $f) {
if ($f['id'] == $forum2->id) {
$this->assertEquals(1, $f['unreadpostscount']);
}
}
// Test discussion without additional posts. There should be only one post (the one created by the discussion).
$posts = mod_forum_external::get_discussion_posts($discussion2->id, 'modified', 'DESC');
$posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
$this->assertEquals(1, count($posts['posts']));
// Test discussion tracking on not tracked forum.
$result = mod_forum_external::view_forum_discussion($discussion1->id);
$result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
$this->assertTrue($result['status']);
$this->assertEmpty($result['warnings']);
// Test posts have not been marked as read.
$posts = mod_forum_external::get_discussion_posts($discussion1->id, 'modified', 'DESC');
$posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
foreach ($posts['posts'] as $post) {
$this->assertNull($post['unread']);
}
// Test discussion tracking on tracked forum.
$result = mod_forum_external::view_forum_discussion($discussion3->id);
$result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
$this->assertTrue($result['status']);
$this->assertEmpty($result['warnings']);
// Test posts have been marked as read.
$posts = mod_forum_external::get_discussion_posts($discussion3->id, 'modified', 'DESC');
$posts = external_api::clean_returnvalue(mod_forum_external::get_discussion_posts_returns(), $posts);
foreach ($posts['posts'] as $post) {
$this->assertFalse($post['unread']);
}
// Check we receive 0 unread posts.
forum_tp_count_forum_unread_posts($forum2cm, $course1, true); // Reset static cache.
$result = mod_forum_external::get_forums_by_courses(array($course1->id));
$result = external_api::clean_returnvalue(mod_forum_external::get_forums_by_courses_returns(), $result);
foreach ($result as $f) {
if ($f['id'] == $forum2->id) {
$this->assertEquals(0, $f['unreadpostscount']);
}
}
}
/**
* Test get forum posts
*/
@@ -1301,4 +1605,98 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
}
}
}
/**
* Test add_discussion_post
*/
public function test_add_discussion_post_private() {
global $DB;
$this->resetAfterTest(true);
self::setAdminUser();
// Create course to add the module.
$course = self::getDataGenerator()->create_course();
// Standard forum.
$record = new stdClass();
$record->course = $course->id;
$forum = self::getDataGenerator()->create_module('forum', $record);
$cm = get_coursemodule_from_id('forum', $forum->cmid, 0, false, MUST_EXIST);
$forumcontext = context_module::instance($forum->cmid);
$generator = self::getDataGenerator()->get_plugin_generator('mod_forum');
// Create an enrol users.
$student1 = self::getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($student1->id, $course->id, 'student');
$student2 = self::getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student');
$teacher1 = self::getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($teacher1->id, $course->id, 'editingteacher');
$teacher2 = self::getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($teacher2->id, $course->id, 'editingteacher');
// Add a new discussion to the forum.
self::setUser($student1);
$record = new stdClass();
$record->course = $course->id;
$record->userid = $student1->id;
$record->forum = $forum->id;
$discussion = $generator->create_discussion($record);
// Have the teacher reply privately.
self::setUser($teacher1);
$post = mod_forum_external::add_discussion_post($discussion->firstpost, 'some subject', 'some text here...', [
[
'name' => 'private',
'value' => true,
],
]);
$post = external_api::clean_returnvalue(mod_forum_external::add_discussion_post_returns(), $post);
$privatereply = $DB->get_record('forum_posts', array('id' => $post['postid']));
$this->assertEquals($student1->id, $privatereply->privatereplyto);
// Bump the time of the private reply to ensure order.
$privatereply->created++;
$privatereply->modified = $privatereply->created;
$DB->update_record('forum_posts', $privatereply);
// The teacher will receive their private reply.
self::setUser($teacher1);
$posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
$posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
$this->assertEquals(2, count($posts['posts']));
$this->assertTrue($posts['posts'][0]['isprivatereply']);
// Another teacher on the course will also receive the private reply.
self::setUser($teacher2);
$posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
$posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
$this->assertEquals(2, count($posts['posts']));
$this->assertTrue($posts['posts'][0]['isprivatereply']);
// The student will receive the private reply.
self::setUser($student1);
$posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
$posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
$this->assertEquals(2, count($posts['posts']));
$this->assertTrue($posts['posts'][0]['isprivatereply']);
// Another student will not receive the private reply.
self::setUser($student2);
$posts = mod_forum_external::get_forum_discussion_posts($discussion->id);
$posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
$this->assertEquals(1, count($posts['posts']));
$this->assertFalse($posts['posts'][0]['isprivatereply']);
// A user cannot reply to a private reply.
self::setUser($teacher2);
$this->expectException('coding_exception');
$post = mod_forum_external::add_discussion_post($privatereply->id, 'some subject', 'some text here...', [
'options' => [
'name' => 'private',
'value' => false,
],
]);
}
}
+22
View File
@@ -304,6 +304,10 @@ class mod_forum_generator extends testing_module_generator {
$record['deleted'] = 0;
}
if (!isset($record['privatereplyto'])) {
$record['privatereplyto'] = 0;
}
$record = (object) $record;
// Add the post.
@@ -346,4 +350,22 @@ class mod_forum_generator extends testing_module_generator {
}
return $post;
}
/**
* Extracted from exporter/post.php
*
* 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
*/
public function get_author_subheading_html(stdClass $exportedauthor, int $timecreated) : string {
$fullname = $exportedauthor->fullname;
$profileurl = $exportedauthor->urls['profile'] ?? null;
$formatteddate = userdate($timecreated, get_string('strftimedaydatetime', 'core_langconfig'));
$name = $profileurl ? "<a href=\"{$profileurl}\">{$fullname}</a>" : $fullname;
$date = "<time>{$formatteddate}</time>";
return get_string('bynameondate', 'mod_forum', ['name' => $name, 'date' => $date]);
}
}
+28 -13
View File
@@ -70,7 +70,7 @@ trait mod_forum_tests_generator_trait {
// Retrieve the post which was created by create_discussion.
$post = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
return array($discussion, $post);
return [$discussion, $post];
}
/**
@@ -108,24 +108,26 @@ trait mod_forum_tests_generator_trait {
* @param stdClass $forum The forum to post in
* @param stdClass $discussion The discussion to post in
* @param stdClass $author The author to post as
* @param array $options Additional options to pass to `create_post`
* @return stdClass The forum post
*/
protected function helper_post_to_discussion($forum, $discussion, $author) {
protected function helper_post_to_discussion($forum, $discussion, $author, array $options = []) {
global $DB;
$generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
// Add a post to the discussion.
$record = new stdClass();
$record->course = $forum->course;
$strre = get_string('re', 'forum');
$record->subject = $strre . ' ' . $discussion->subject;
$record->userid = $author->id;
$record->forum = $forum->id;
$record->discussion = $discussion->id;
$record->mailnow = 1;
$record = array_merge([
'course' => $forum->course,
'subject' => "{$strre} {$discussion->subject}",
'userid' => $author->id,
'forum' => $forum->id,
'discussion' => $discussion->id,
'mailnow' => 1,
], $options);
$post = $generator->create_post($record);
$post = $generator->create_post((object) $record);
return $post;
}
@@ -135,25 +137,38 @@ trait mod_forum_tests_generator_trait {
*
* @param stdClass $parent The post being replied to
* @param stdClass $author The author to post as
* @param array $options Additional options to pass to `create_post`
* @return stdClass The forum post
*/
protected function helper_reply_to_post($parent, $author) {
protected function helper_reply_to_post($parent, $author, array $options = []) {
global $DB;
$generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
// Add a post to the discussion.
$strre = get_string('re', 'forum');
$record = (object) [
$record = (object) array_merge([
'discussion' => $parent->discussion,
'parent' => $parent->id,
'userid' => $author->id,
'mailnow' => 1,
'subject' => $strre . ' ' . $parent->subject,
];
], $options);
$post = $generator->create_post($record);
return $post;
}
/**
* Gets the role id from it's shortname.
*
* @param string $roleshortname
* @return int
*/
protected function get_role_id($roleshortname) {
global $DB;
return $DB->get_field('role', 'id', ['shortname' => $roleshortname]);
}
}
-166
View File
@@ -1,166 +0,0 @@
<?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/>.
/**
* Helper functions used by several tests.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
/**
* Helper functions used by several tests.
*
* @copyright 2018 Andrew Nicols <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait helper {
/**
* Helper to create the required number of users in the specified
* course.
* Users are enrolled as students.
*
* @param stdClass $course The course object
* @param integer $count The number of users to create
* @return array The users created
*/
protected function helper_create_users($course, $count) {
$users = array();
for ($i = 0; $i < $count; $i++) {
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id);
$users[] = $user;
}
return $users;
}
/**
* Create a new discussion and post within the specified forum, as the
* specified author.
*
* @param stdClass $forum The forum to post in
* @param stdClass $author The author to post as
* @return array An array containing the discussion object, and the post object
*/
protected function helper_post_to_forum($forum, $author) {
global $DB;
$generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
// Create a discussion in the forum, and then add a post to that discussion.
$record = new stdClass();
$record->course = $forum->course;
$record->userid = $author->id;
$record->forum = $forum->id;
$discussion = $generator->create_discussion($record);
// Retrieve the post which was created by create_discussion.
$post = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
return array($discussion, $post);
}
/**
* Update the post time for the specified post by $factor.
*
* @param stdClass $post The post to update
* @param int $factor The amount to update by
*/
protected function helper_update_post_time($post, $factor) {
global $DB;
// Update the post to have a created in the past.
$DB->set_field('forum_posts', 'created', $post->created + $factor, array('id' => $post->id));
}
/**
* Update the subscription time for the specified user/discussion by $factor.
*
* @param stdClass $user The user to update
* @param stdClass $discussion The discussion to update for this user
* @param int $factor The amount to update by
*/
protected function helper_update_subscription_time($user, $discussion, $factor) {
global $DB;
$sub = $DB->get_record('forum_discussion_subs', array('userid' => $user->id, 'discussion' => $discussion->id));
// Update the subscription to have a preference in the past.
$DB->set_field('forum_discussion_subs', 'preference', $sub->preference + $factor, array('id' => $sub->id));
}
/**
* Create a new post within an existing discussion, as the specified author.
*
* @param stdClass $forum The forum to post in
* @param stdClass $discussion The discussion to post in
* @param stdClass $author The author to post as
* @return stdClass The forum post
*/
protected function helper_post_to_discussion($forum, $discussion, $author) {
global $DB;
$generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
// Add a post to the discussion.
$record = new stdClass();
$record->course = $forum->course;
$strre = get_string('re', 'forum');
$record->subject = $strre . ' ' . $discussion->subject;
$record->userid = $author->id;
$record->forum = $forum->id;
$record->discussion = $discussion->id;
$record->mailnow = 1;
$post = $generator->create_post($record);
return $post;
}
/**
* Create a new post within an existing discussion, as the specified author.
*
* @param stdClass $parent The post being replied to
* @param stdClass $author The author to post as
* @return stdClass The forum post
*/
protected function helper_reply_to_post($parent, $author) {
global $DB;
$generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
// Add a post to the discussion.
$strre = get_string('re', 'forum');
$record = (object) [
'discussion' => $parent->discussion,
'parent' => $parent->id,
'userid' => $author->id,
'mailnow' => 1,
'subject' => $strre . ' ' . $parent->subject,
];
$post = $generator->create_post($record);
return $post;
}
}
+120
View File
@@ -1712,6 +1712,81 @@ class mod_forum_lib_testcase extends advanced_testcase {
$this->assertEquals($discussionid, $row->discussion);
}
}
/**
* Test the reply count when used with private replies.
*/
public function test_forum_count_discussion_replies_private() {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
$context = context_module::instance($forum->cmid);
$cm = get_coursemodule_from_instance('forum', $forum->id);
$student = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($student->id, $course->id);
$teacher = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($teacher->id, $course->id);
$privilegeduser = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($privilegeduser->id, $course->id, 'editingteacher');
$otheruser = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($otheruser->id, $course->id);
$generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
// Create a discussion with some replies.
$record = new stdClass();
$record->course = $forum->course;
$record->forum = $forum->id;
$record->userid = $student->id;
$discussion = $generator->create_discussion($record);
$replycount = 5;
$replyto = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
// Create a couple of standard replies.
$post = new stdClass();
$post->userid = $student->id;
$post->discussion = $discussion->id;
$post->parent = $replyto->id;
for ($i = 0; $i < $replycount; $i++) {
$post = $generator->create_post($post);
}
// Create a private reply post from the teacher back to the student.
$reply = new stdClass();
$reply->userid = $teacher->id;
$reply->discussion = $discussion->id;
$reply->parent = $replyto->id;
$reply->privatereplyto = $replyto->userid;
$generator->create_post($reply);
// The user is the author of the private reply.
$this->setUser($teacher->id);
$counts = forum_count_discussion_replies($forum->id);
$this->assertEquals($replycount + 1, $counts[$discussion->id]->replies);
// The user is the intended recipient.
$this->setUser($student->id);
$counts = forum_count_discussion_replies($forum->id);
$this->assertEquals($replycount + 1, $counts[$discussion->id]->replies);
// The user is not the author or recipient, but does have the readprivatereplies capability.
$this->setUser($privilegeduser->id);
$counts = forum_count_discussion_replies($forum->id, "", -1, -1, 0, true);
$this->assertEquals($replycount + 1, $counts[$discussion->id]->replies);
// The user is not allowed to view this post.
$this->setUser($otheruser->id);
$counts = forum_count_discussion_replies($forum->id);
$this->assertEquals($replycount, $counts[$discussion->id]->replies);
}
public function test_discussion_pinned_sort() {
list($forum, $discussionids) = $this->create_multiple_discussions_with_replies(10, 5);
$cm = get_coursemodule_from_instance('forum', $forum->id);
@@ -3519,4 +3594,49 @@ class mod_forum_lib_testcase extends advanced_testcase {
$this->assertEquals(mod_forum_get_completion_active_rule_descriptions($moddefaults), $activeruledescriptions);
$this->assertEquals(mod_forum_get_completion_active_rule_descriptions(new stdClass()), []);
}
/**
* Test the forum_post_is_visible_privately function used in private replies.
*/
public function test_forum_post_is_visible_privately() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
$context = context_module::instance($forum->cmid);
$cm = get_coursemodule_from_instance('forum', $forum->id);
$author = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($author->id, $course->id);
$recipient = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($recipient->id, $course->id);
$privilegeduser = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($privilegeduser->id, $course->id, 'editingteacher');
$otheruser = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($otheruser->id, $course->id);
// Fake a post - this does not need to be persisted to the DB.
$post = new \stdClass();
$post->userid = $author->id;
$post->privatereplyto = $recipient->id;
// The user is the author.
$this->setUser($author->id);
$this->assertTrue(forum_post_is_visible_privately($post, $cm));
// The user is the intended recipient.
$this->setUser($recipient->id);
$this->assertTrue(forum_post_is_visible_privately($post, $cm));
// The user is not the author or recipient, but does have the readprivatereplies capability.
$this->setUser($privilegeduser->id);
$this->assertTrue(forum_post_is_visible_privately($post, $cm));
// The user is not allowed to view this post.
$this->setUser($otheruser->id);
$this->assertFalse(forum_post_is_visible_privately($post, $cm));
}
}
+50
View File
@@ -331,6 +331,56 @@ class mod_forum_mail_testcase extends advanced_testcase {
$this->send_notifications_and_assert($recipient, [$post]);
}
/**
* Ensure that private replies are not sent to users with an automatic subscription unless they are an expected
* recipient.
*/
public function test_automatic_with_private_reply() {
$this->resetAfterTest(true);
// Create a course, with a forum.
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
'forcesubscribe' => FORUM_INITIALSUBSCRIBE,
]);
[$student, $otherstudent] = $this->helper_create_users($course, 2, 'student');
[$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher');
[$discussion, $post] = $this->helper_post_to_forum($forum, $student);
$reply = $this->helper_post_to_discussion($forum, $discussion, $teacher, [
'privatereplyto' => $student->id,
]);
// The private reply is queued to all messages as reply visibility may change between queueing, and sending.
$expect = [
(object) [
'userid' => $student->id,
'messages' => 2,
],
(object) [
'userid' => $otherstudent->id,
'messages' => 2,
],
(object) [
'userid' => $teacher->id,
'messages' => 2,
],
(object) [
'userid' => $otherteacher->id,
'messages' => 2,
],
];
$this->queue_tasks_and_assert($expect);
// The actual messages sent will respect private replies.
$this->send_notifications_and_assert($student, [$post, $reply]);
$this->send_notifications_and_assert($teacher, [$post, $reply]);
$this->send_notifications_and_assert($otherteacher, [$post, $reply]);
$this->send_notifications_and_assert($otherstudent, [$post]);
}
public function test_optional() {
$this->resetAfterTest(true);
@@ -35,6 +35,8 @@ use mod_forum\local\managers\capability as capability_manager;
* @package mod_forum
* @copyright 2019 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \mod_forum\local\managers\capability
* @covers ::<!public>
*/
class mod_forum_managers_capability_testcase extends advanced_testcase {
// Make use of the test generator trait.
@@ -157,6 +159,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_subscribe_to_forum.
*
* @covers ::can_subscribe_to_forum
*/
public function test_can_subscribe_to_forum() {
$this->resetAfterTest();
@@ -171,6 +175,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_create_discussions.
*
* @covers ::can_create_discussions
*/
public function test_can_create_discussions() {
$this->resetAfterTest();
@@ -238,6 +244,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_access_all_groups.
*
* @covers ::can_access_all_groups
*/
public function test_can_access_all_groups() {
$this->resetAfterTest();
@@ -255,6 +263,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_access_group.
*
* @covers ::can_access_group
*/
public function test_can_access_group() {
$this->resetAfterTest();
@@ -277,6 +287,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_view_discussions.
*
* @covers ::can_view_discussions
*/
public function test_can_view_discussions() {
$this->resetAfterTest();
@@ -294,6 +306,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_move_discussions.
*
* @covers ::can_move_discussions
*/
public function test_can_move_discussions() {
$this->resetAfterTest();
@@ -316,6 +330,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_pin_discussions.
*
* @covers ::can_pin_discussions
*/
public function test_can_pin_discussions() {
$this->resetAfterTest();
@@ -333,6 +349,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_split_discussions.
*
* @covers ::can_split_discussions
*/
public function test_can_split_discussions() {
$this->resetAfterTest();
@@ -355,6 +373,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_export_discussions.
*
* @covers ::can_export_discussions
*/
public function test_can_export_discussions() {
global $CFG;
@@ -378,6 +398,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_manually_control_post_read_status.
*
* @covers ::can_manually_control_post_read_status
*/
public function test_can_manually_control_post_read_status() {
global $CFG, $DB;
@@ -405,6 +427,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test must_post_before_viewing_discussion.
*
* @covers ::must_post_before_viewing_discussion
*/
public function test_must_post_before_viewing_discussion() {
$this->resetAfterTest();
@@ -434,6 +458,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_subscribe_to_discussion.
*
* @covers ::can_subscribe_to_discussion
*/
public function test_can_subscribe_to_discussion() {
$this->resetAfterTest();
@@ -449,6 +475,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_move_discussion.
*
* @covers ::can_move_discussion
*/
public function test_can_move_discussion() {
$this->resetAfterTest();
@@ -472,6 +500,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_pin_discussion.
*
* @covers ::can_pin_discussion
*/
public function test_can_pin_discussion() {
$this->resetAfterTest();
@@ -490,6 +520,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_post_in_discussion.
*
* @covers ::can_post_in_discussion
*/
public function test_can_post_in_discussion() {
$this->resetAfterTest();
@@ -594,6 +626,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_edit_post.
*
* @covers ::can_edit_post
*/
public function test_can_edit_post() {
global $CFG;
@@ -676,6 +710,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_delete_post.
*
* @covers ::can_delete_post
*/
public function test_can_delete_post() {
global $CFG;
@@ -740,6 +776,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_split_post.
*
* @covers ::can_split_post
*/
public function test_can_split_post() {
$this->resetAfterTest();
@@ -777,10 +815,20 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
// Can't split a single discussion forum.
$this->assertFalse($capabilitymanager->can_split_post($user, $discussion, $post));
// Make the post a private reply.
$post = $this->entityfactory->get_post_from_stdclass(
(object) array_merge((array) $this->postrecord, ['parent' => 5, 'privatereplyto' => $user->id])
);
// Can't split at a private reply.
$this->assertFalse($capabilitymanager->can_split_post($user, $discussion, $post));
}
/**
* Test can_reply_to_post.
*
* @covers ::can_reply_to_post
*/
public function test_can_reply_to_post() {
$this->resetAfterTest();
@@ -882,10 +930,111 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
$this->getDataGenerator()->create_group_member(['userid' => $user->id, 'groupid' => $group->id]);
$this->assertTrue($capabilitymanager->can_reply_to_post($user, $discussion, $post));
// Make the post a private reply.
$post = $this->entityfactory->get_post_from_stdclass(
(object) array_merge((array) $this->postrecord, ['parent' => 5, 'privatereplyto' => $user->id])
);
// Can't reply to a a private reply.
$this->assertFalse($capabilitymanager->can_reply_to_post($user, $discussion, $post));
}
/**
* Ensure that can_reply_privately_to_post works as expected.
*
* @covers ::can_reply_privately_to_post
*/
public function test_can_reply_privately_to_post() {
$this->resetAfterTest();
$forum = $this->create_forum();
$discussion = $this->discussion;
$user = $this->user;
$post = $this->post;
$capabilitymanager = $this->managerfactory->get_capability_manager($forum);
// Without the capability, and with a standard post, it is not possible to reply privately.
$this->prevent_capability('mod/forum:postprivatereply');
$this->assertFalse($capabilitymanager->can_reply_privately_to_post($this->user, $post));
// With the capability, and a standard post, it is possible to reply privately.
$this->give_capability('mod/forum:postprivatereply');
$this->assertTrue($capabilitymanager->can_reply_privately_to_post($this->user, $post));
// Make the post a private reply.
$post = $this->entityfactory->get_post_from_stdclass(
(object) array_merge((array) $this->postrecord, ['parent' => 5, 'privatereplyto' => $user->id])
);
// Can't ever reply to a a private reply.
$this->assertFalse($capabilitymanager->can_reply_privately_to_post($user, $post));
}
/**
* Ensure that can_view_post works as expected.
*
* @covers ::can_view_post
*/
public function test_can_view_post() {
$this->resetAfterTest();
$forum = $this->create_forum();
$capabilitymanager = $this->managerfactory->get_capability_manager($forum);
$user = $this->user;
$otheruser = $this->getDataGenerator()->create_user();
$discussion = $this->discussion;
$post = $this->post;
$privatepost = $this->entityfactory->get_post_from_stdclass(
(object) array_merge((array) $this->postrecord, ['parent' => $post->get_id(), 'privatereplyto' => $otheruser->id])
);
$this->prevent_capability('mod/forum:readprivatereplies');
$this->assertFalse($capabilitymanager->can_view_post($user, $discussion, $privatepost));
}
/**
* Ensure that can_view_post_shell considers private replies correctly.
*
* @covers ::can_view_post_shell
*/
public function test_can_view_post_shell() {
$this->resetAfterTest();
$forum = $this->create_forum();
$capabilitymanager = $this->managerfactory->get_capability_manager($forum);
$user = $this->user;
$otheruser = $this->getDataGenerator()->create_user();
$discussion = $this->discussion;
$post = $this->post;
$privatepost = $this->entityfactory->get_post_from_stdclass(
(object) array_merge((array) $this->postrecord, ['parent' => $post->get_id(), 'privatereplyto' => $otheruser->id])
);
$privateposttome = $this->entityfactory->get_post_from_stdclass(
(object) array_merge((array) $this->postrecord, ['parent' => $post->get_id(), 'privatereplyto' => $user->id])
);
// Can always view public replies, and those to me.
$this->prevent_capability('mod/forum:readprivatereplies');
$this->assertTrue($capabilitymanager->can_view_post_shell($this->user, $post));
$this->assertTrue($capabilitymanager->can_view_post_shell($this->user, $privateposttome));
$this->assertFalse($capabilitymanager->can_view_post_shell($this->user, $privatepost));
$this->give_capability('mod/forum:readprivatereplies');
$this->assertTrue($capabilitymanager->can_view_post_shell($this->user, $post));
$this->assertTrue($capabilitymanager->can_view_post_shell($this->user, $privateposttome));
$this->assertTrue($capabilitymanager->can_view_post_shell($this->user, $privatepost));
}
/**
* Test can_export_post.
*
* @covers ::can_export_post
*/
public function test_can_export_post() {
global $CFG;
@@ -922,6 +1071,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_view_participants.
*
* @covers ::can_view_participants
*/
public function test_can_view_participants() {
$this->resetAfterTest();
@@ -961,6 +1112,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_view_hidden_posts.
*
* @covers ::can_view_hidden_posts
*/
public function test_can_view_hidden_posts() {
$this->resetAfterTest();
@@ -978,6 +1131,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_manage_forum.
*
* @covers ::can_manage_forum
*/
public function test_can_manage_forum() {
$this->resetAfterTest();
@@ -995,6 +1150,8 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
/**
* Test can_manage_tags.
*
* @covers ::can_manage_tags
*/
public function test_can_manage_tags() {
global $DB;
@@ -1012,4 +1169,21 @@ class mod_forum_managers_capability_testcase extends advanced_testcase {
assign_capability('moodle/tag:manage', CAP_ALLOW, $roleid, $context->id, true);
$this->assertTrue($capabilitymanager->can_manage_tags($user));
}
/**
* Ensure that the can_view_any_private_reply works as expected.
*
* @covers ::can_view_any_private_reply
*/
public function test_can_view_any_private_reply() {
$this->resetAfterTest();
$forum = $this->create_forum();
$capabilitymanager = $this->managerfactory->get_capability_manager($forum);
$this->give_capability('mod/forum:readprivatereplies');
$this->assertTrue($capabilitymanager->can_view_any_private_reply($this->user));
$this->prevent_capability('mod/forum:readprivatereplies');
$this->assertFalse($capabilitymanager->can_view_any_private_reply($this->user));
}
}
+89 -2
View File
@@ -26,7 +26,7 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/helper.php');
require_once(__DIR__ . '/generator_trait.php');
require_once($CFG->dirroot . '/rating/lib.php');
use \mod_forum\privacy\provider;
@@ -45,7 +45,7 @@ class mod_forum_privacy_provider_testcase extends \core_privacy\tests\provider_t
// Include the mod_forum test helpers.
// This includes functions to create forums, users, discussions, and posts.
use helper;
use mod_forum_tests_generator_trait;
// Include the privacy helper trait for the ratings API.
use \core_rating\phpunit\privacy_helper;
@@ -394,6 +394,93 @@ class mod_forum_privacy_provider_testcase extends \core_privacy\tests\provider_t
$this->assertEquals(0, $post->deleted);
}
/**
* Test private reply in a range of scenarios.
*/
public function test_user_private_reply() {
global $DB;
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
$cm = get_coursemodule_from_instance('forum', $forum->id);
$context = \context_module::instance($cm->id);
[$student, $otherstudent] = $this->helper_create_users($course, 2, 'student');
[$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher');
[$discussion, $post] = $this->helper_post_to_forum($forum, $student);
$reply = $this->helper_reply_to_post($post, $teacher, [
'privatereplyto' => $student->id,
]);
// Testing as user $student.
$this->setUser($student);
// Retrieve all contexts - only this context should be returned.
$contextlist = $this->get_contexts_for_userid($student->id, 'mod_forum');
$this->assertCount(1, $contextlist);
$this->assertEquals($context, $contextlist->current());
// Export all of the data for the context.
$this->export_context_data_for_user($student->id, $context, 'mod_forum');
$writer = \core_privacy\local\request\writer::with_context($context);
$this->assertTrue($writer->has_any_data());
// The initial post and reply will be included.
$this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);
$this->assert_post_data($reply, $writer->get_data($this->get_subcontext($forum, $discussion, $reply)), $writer);
// Testing as user $teacher.
\core_privacy\local\request\writer::reset();
$this->setUser($teacher);
// Retrieve all contexts - only this context should be returned.
$contextlist = $this->get_contexts_for_userid($teacher->id, 'mod_forum');
$this->assertCount(1, $contextlist);
$this->assertEquals($context, $contextlist->current());
// Export all of the data for the context.
$this->export_context_data_for_user($teacher->id, $context, 'mod_forum');
$writer = \core_privacy\local\request\writer::with_context($context);
$this->assertTrue($writer->has_any_data());
// The reply will be included.
$this->assert_post_data($post, $writer->get_data($this->get_subcontext($forum, $discussion, $post)), $writer);
$this->assert_post_data($reply, $writer->get_data($this->get_subcontext($forum, $discussion, $reply)), $writer);
// Testing as user $otherteacher.
// The user was not involved in any of the conversation.
\core_privacy\local\request\writer::reset();
$this->setUser($otherteacher);
// Retrieve all contexts - only this context should be returned.
$contextlist = $this->get_contexts_for_userid($otherteacher->id, 'mod_forum');
$this->assertCount(0, $contextlist);
// Export all of the data for the context.
$this->export_context_data_for_user($otherteacher->id, $context, 'mod_forum');
$writer = \core_privacy\local\request\writer::with_context($context);
// The user has none of the discussion.
$this->assertEmpty($writer->get_data($this->get_subcontext($forum, $discussion)));
// Testing as user $otherstudent.
// The user was not involved in any of the conversation.
\core_privacy\local\request\writer::reset();
$this->setUser($otherstudent);
// Retrieve all contexts - only this context should be returned.
$contextlist = $this->get_contexts_for_userid($otherstudent->id, 'mod_forum');
$this->assertCount(0, $contextlist);
// Export all of the data for the context.
$this->export_context_data_for_user($otherstudent->id, $context, 'mod_forum');
$writer = \core_privacy\local\request\writer::with_context($context);
// The user has none of the discussion.
$this->assertEmpty($writer->get_data($this->get_subcontext($forum, $discussion)));
}
/**
* Test that the rating of another users content will have only the
* rater's information returned.
+242
View File
@@ -0,0 +1,242 @@
<?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/>.
/**
* Tests for private reply functionality.
*
* @package mod_forum
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/forum/lib.php');
require_once($CFG->dirroot . '/mod/forum/locallib.php');
require_once(__DIR__ . '/generator_trait.php');
/**
* Tests for private reply functionality.
*
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class private_replies extends advanced_testcase {
use mod_forum_tests_generator_trait;
/**
* Setup before tests.
*/
public function setUp() {
// We must clear the subscription caches. This has to be done both before each test, and after in case of other
// tests using these functions.
\mod_forum\subscriptions::reset_forum_cache();
}
/**
* Tear down after tests.
*/
public function tearDown() {
// We must clear the subscription caches. This has to be done both before each test, and after in case of other
// tests using these functions.
\mod_forum\subscriptions::reset_forum_cache();
}
/**
* Ensure that the forum_post_is_visible_privately function reports that a post is visible to a user when another
* user wrote the post, and it is not private.
*/
public function test_forum_post_is_visible_privately_not_private() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
]);
[$student] = $this->helper_create_users($course, 1, 'student');
[$teacher] = $this->helper_create_users($course, 1, 'teacher');
[$discussion] = $this->helper_post_to_forum($forum, $teacher);
$post = $this->helper_post_to_discussion($forum, $discussion, $teacher);
$this->setUser($student);
$cm = get_coursemodule_from_instance('forum', $forum->id);
$this->assertTrue(forum_post_is_visible_privately($post, $cm));
}
/**
* Ensure that the forum_post_is_visible_privately function reports that a post is visible to a user when another
* user wrote the post, and the user under test is the intended recipient.
*/
public function test_forum_post_is_visible_privately_private_to_user() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
]);
[$student] = $this->helper_create_users($course, 1, 'student');
[$teacher] = $this->helper_create_users($course, 1, 'teacher');
[$discussion] = $this->helper_post_to_forum($forum, $teacher);
$post = $this->helper_post_to_discussion($forum, $discussion, $teacher, [
'privatereplyto' => $student->id,
]);
$this->setUser($student);
$cm = get_coursemodule_from_instance('forum', $forum->id);
$this->assertTrue(forum_post_is_visible_privately($post, $cm));
}
/**
* Ensure that the forum_post_is_visible_privately function reports that a post is visible to a user when another
* user wrote the post, and the user under test is a role with the view capability.
*/
public function test_forum_post_is_visible_privately_private_to_user_view_as_teacher() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
]);
[$student] = $this->helper_create_users($course, 1, 'student');
[$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher');
[$discussion] = $this->helper_post_to_forum($forum, $teacher);
$post = $this->helper_post_to_discussion($forum, $discussion, $teacher, [
'privatereplyto' => $student->id,
]);
$this->setUser($otherteacher);
$cm = get_coursemodule_from_instance('forum', $forum->id);
$this->assertTrue(forum_post_is_visible_privately($post, $cm));
}
/**
* Ensure that the forum_post_is_visible_privately function reports that a post is not visible to a user when
* another user wrote the post, and the user under test is a role without the view capability.
*/
public function test_forum_post_is_visible_privately_private_to_user_view_as_other_student() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
]);
[$student, $otherstudent] = $this->helper_create_users($course, 2, 'student');
[$teacher] = $this->helper_create_users($course, 1, 'teacher');
[$discussion] = $this->helper_post_to_forum($forum, $teacher);
$post = $this->helper_post_to_discussion($forum, $discussion, $teacher, [
'privatereplyto' => $student->id,
]);
$this->setUser($otherstudent);
$cm = get_coursemodule_from_instance('forum', $forum->id);
$this->assertFalse(forum_post_is_visible_privately($post, $cm));
}
/**
* Ensure that the forum_post_is_visible_privately function reports that a post is visible to a user who wrote a
* private reply, but not longer holds the view capability.
*/
public function test_forum_post_is_visible_privately_private_to_user_view_as_author() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
]);
[$student] = $this->helper_create_users($course, 1, 'student');
[$teacher] = $this->helper_create_users($course, 1, 'teacher');
[$discussion] = $this->helper_post_to_forum($forum, $teacher);
$post = $this->helper_post_to_discussion($forum, $discussion, $teacher, [
'privatereplyto' => $student->id,
]);
unassign_capability('mod/forum:readprivatereplies', $this->get_role_id('teacher'));
$this->setUser($teacher);
$cm = get_coursemodule_from_instance('forum', $forum->id);
$this->assertTrue(forum_post_is_visible_privately($post, $cm));
}
/**
* Ensure that the forum_user_can_reply_privately returns true for a teacher replying to a forum post.
*/
public function test_forum_user_can_reply_privately_as_teacher() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
]);
[$student] = $this->helper_create_users($course, 1, 'student');
[$teacher] = $this->helper_create_users($course, 1, 'teacher');
[, $post] = $this->helper_post_to_forum($forum, $student);
$this->setUser($teacher);
$cm = get_coursemodule_from_instance('forum', $forum->id);
$context = \context_module::instance($cm->id);
$this->assertTrue(forum_user_can_reply_privately($context, $post));
}
/**
* Ensure that the forum_user_can_reply_privately returns true for a teacher replying to a forum post.
*/
public function test_forum_user_can_reply_privately_as_student() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
]);
[$student, $otherstudent] = $this->helper_create_users($course, 2, 'student');
[, $post] = $this->helper_post_to_forum($forum, $student);
$this->setUser($otherstudent);
$cm = get_coursemodule_from_instance('forum', $forum->id);
$context = \context_module::instance($cm->id);
$this->assertFalse(forum_user_can_reply_privately($context, $post));
}
/**
* Ensure that the forum_user_can_reply_privately returns false where the parent post is already a private reply.
*/
public function test_forum_user_can_reply_privately_parent_is_already_private() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
]);
[$student] = $this->helper_create_users($course, 1, 'student');
[$teacher] = $this->helper_create_users($course, 1, 'teacher');
[$discussion] = $this->helper_post_to_forum($forum, $student);
$post = $this->helper_post_to_discussion($forum, $discussion, $teacher, ['privatereplyto' => $student->id]);
$this->setUser($teacher);
$cm = get_coursemodule_from_instance('forum', $forum->id);
$context = \context_module::instance($cm->id);
$this->assertFalse(forum_user_can_reply_privately($context, $post));
}
}
+2 -2
View File
@@ -25,7 +25,7 @@
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/helper.php');
require_once(__DIR__ . '/generator_trait.php');
require_once("{$CFG->dirroot}/mod/forum/rsslib.php");
/**
@@ -37,7 +37,7 @@ require_once("{$CFG->dirroot}/mod/forum/rsslib.php");
class mod_forum_rsslib_testcase extends advanced_testcase {
// Include the mod_forum test helpers.
// This includes functions to create forums, users, discussions, and posts.
use helper;
use mod_forum_tests_generator_trait;
/**
* Ensure that deleted posts are not included.
+3 -3
View File
@@ -25,13 +25,13 @@
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/forum/lib.php');
require_once(__DIR__ . '/helper.php');
require_once(__DIR__ . '/generator_trait.php');
require_once("{$CFG->dirroot}/mod/forum/lib.php");
class mod_forum_subscriptions_testcase extends advanced_testcase {
// Include the mod_forum test helpers.
// This includes functions to create forums, users, discussions, and posts.
use helper;
use mod_forum_tests_generator_trait;
/**
* Test setUp.
+52 -13
View File
@@ -24,32 +24,31 @@
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/generator_trait.php');
/**
* The forum vault tests.
*
* @package mod_forum
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \mod_forum\local\vaults\forum
*/
class mod_forum_vaults_forum_testcase extends advanced_testcase {
/** @var \mod_forum\local\vaults\discussion */
private $vault;
/**
* Set up function for tests.
*/
public function setUp() {
$vaultfactory = \mod_forum\local\container::get_vault_factory();
$this->vault = $vaultfactory->get_forum_vault();
}
// Make use of the test generator trait.
use mod_forum_tests_generator_trait;
/**
* Test get_from_id.
*
* @covers ::get_from_id
* @covers ::<!public>
*/
public function test_get_from_id() {
$this->resetAfterTest();
$vault = $this->vault;
$vaultfactory = \mod_forum\local\container::get_vault_factory();
$vault = $vaultfactory->get_forum_vault();
$datagenerator = $this->getDataGenerator();
$user = $datagenerator->create_user();
$course = $datagenerator->create_course();
@@ -62,11 +61,15 @@ class mod_forum_vaults_forum_testcase extends advanced_testcase {
/**
* Test get_from_course_module_id.
*
* @covers ::get_from_course_module_id
* @covers ::<!public>
*/
public function test_get_from_course_module_id() {
$this->resetAfterTest();
$vault = $this->vault;
$vaultfactory = \mod_forum\local\container::get_vault_factory();
$vault = $vaultfactory->get_forum_vault();
$datagenerator = $this->getDataGenerator();
$user = $datagenerator->create_user();
$course = $datagenerator->create_course();
@@ -85,11 +88,15 @@ class mod_forum_vaults_forum_testcase extends advanced_testcase {
/**
* Test get_from_course_module_ids.
*
* @covers ::get_from_course_module_ids
* @covers ::<!public>
*/
public function test_get_from_course_module_ids() {
$this->resetAfterTest();
$vault = $this->vault;
$vaultfactory = \mod_forum\local\container::get_vault_factory();
$vault = $vaultfactory->get_forum_vault();
$datagenerator = $this->getDataGenerator();
$user = $datagenerator->create_user();
$course = $datagenerator->create_course();
@@ -117,4 +124,36 @@ class mod_forum_vaults_forum_testcase extends advanced_testcase {
$this->assertCount(1, $entities);
$this->assertEquals($forum1->id, $entities[0]->get_id());
}
/**
* Test get_from_post_id.
*
* @covers ::get_from_post_id
* @covers ::<!public>
*/
public function test_get_from_post_id() {
$this->resetAfterTest();
$vaultfactory = \mod_forum\local\container::get_vault_factory();
$vault = $vaultfactory->get_forum_vault();
$datagenerator = $this->getDataGenerator();
$user = $datagenerator->create_user();
$course = $datagenerator->create_course();
$forum = $datagenerator->create_module('forum', ['course' => $course->id]);
[$discussion, $post] = $this->helper_post_to_forum($forum, $user);
$reply = $this->helper_reply_to_post($post, $user);
$otherforum = $datagenerator->create_module('forum', ['course' => $course->id]);
[$otherdiscussion, $otherpost] = $this->helper_post_to_forum($otherforum, $user);
$otherreply = $this->helper_reply_to_post($otherpost, $user);
$entity = $vault->get_from_post_id($post->id);
$this->assertEquals($forum->id, $entity->get_id());
$entity = $vault->get_from_post_id($reply->id);
$this->assertEquals($forum->id, $entity->get_id());
$this->assertEmpty($vault->get_from_post_id(-1));
}
}
+423 -25
View File
@@ -91,17 +91,63 @@ class mod_forum_vaults_post_testcase extends advanced_testcase {
$post3 = $this->helper_reply_to_post($post1, $user);
[$discussion2, $post4] = $this->helper_post_to_forum($forum, $user);
$entities = array_values($this->vault->get_from_discussion_id($discussion1->id));
$entities = array_values($this->vault->get_from_discussion_id($user, $discussion1->id, false));
$this->assertCount(3, $entities);
$this->assertEquals($post1->id, $entities[0]->get_id());
$this->assertEquals($post2->id, $entities[1]->get_id());
$this->assertEquals($post3->id, $entities[2]->get_id());
$entities = array_values($this->vault->get_from_discussion_id($discussion1->id + 1000));
$entities = array_values($this->vault->get_from_discussion_id($user, $discussion1->id + 1000, false));
$this->assertCount(0, $entities);
}
/**
* Ensure that selecting posts in a discussion only returns posts that the user can see, when considering private
* replies.
*
* @covers ::get_from_discussion_id
* @covers ::<!public>
*/
public function test_get_from_discussion_id_private_replies() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
]);
[$student, $otherstudent] = $this->helper_create_users($course, 2, 'student');
[$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher');
[$discussion, $post] = $this->helper_post_to_forum($forum, $teacher);
$reply = $this->helper_post_to_discussion($forum, $discussion, $teacher, [
'privatereplyto' => $student->id,
]);
// The user is the author.
$entities = array_values($this->vault->get_from_discussion_id($teacher, $discussion->id, true));
$this->assertCount(2, $entities);
$this->assertEquals($post->id, $entities[0]->get_id());
$this->assertEquals($reply->id, $entities[1]->get_id());
// The user is the intended recipient.
$entities = array_values($this->vault->get_from_discussion_id($student, $discussion->id, false));
$this->assertCount(2, $entities);
$this->assertEquals($post->id, $entities[0]->get_id());
$this->assertEquals($reply->id, $entities[1]->get_id());
// The user is another teacher..
$entities = array_values($this->vault->get_from_discussion_id($otherteacher, $discussion->id, true));
$this->assertCount(2, $entities);
$this->assertEquals($post->id, $entities[0]->get_id());
$this->assertEquals($reply->id, $entities[1]->get_id());
// The user is a different student.
$entities = array_values($this->vault->get_from_discussion_id($otherstudent, $discussion->id, false));
$this->assertCount(1, $entities);
$this->assertEquals($post->id, $entities[0]->get_id());
}
/**
* Test get_from_discussion_ids when no discussion ids were provided.
*
@@ -115,7 +161,7 @@ class mod_forum_vaults_post_testcase extends advanced_testcase {
$course = $datagenerator->create_course();
$forum = $datagenerator->create_module('forum', ['course' => $course->id]);
$this->assertEquals([], $this->vault->get_from_discussion_ids([]));
$this->assertEquals([], $this->vault->get_from_discussion_ids($user, [], false));
}
/**
@@ -136,7 +182,7 @@ class mod_forum_vaults_post_testcase extends advanced_testcase {
$post3 = $this->helper_reply_to_post($post1, $user);
[$discussion2, $post4] = $this->helper_post_to_forum($forum, $user);
$entities = array_values($this->vault->get_from_discussion_ids([$discussion1->id]));
$entities = array_values($this->vault->get_from_discussion_ids($user, [$discussion1->id], false));
usort($entities, function($a, $b) {
return $a <=> $b;
});
@@ -145,7 +191,7 @@ class mod_forum_vaults_post_testcase extends advanced_testcase {
$this->assertEquals($post2->id, $entities[1]->get_id());
$this->assertEquals($post3->id, $entities[2]->get_id());
$entities = array_values($this->vault->get_from_discussion_ids([$discussion1->id, $discussion2->id]));
$entities = array_values($this->vault->get_from_discussion_ids($user, [$discussion1->id, $discussion2->id], false));
usort($entities, function($a, $b) {
return $a <=> $b;
});
@@ -156,6 +202,59 @@ class mod_forum_vaults_post_testcase extends advanced_testcase {
$this->assertEquals($post4->id, $entities[3]->get_id());
}
/**
* Ensure that selecting posts in a discussion only returns posts that the user can see, when considering private
* replies.
*
* @covers ::get_from_discussion_ids
* @covers ::<!public>
*/
public function test_get_from_discussion_ids_private_replies() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
]);
[$student, $otherstudent] = $this->helper_create_users($course, 2, 'student');
[$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher');
[$discussion, $post] = $this->helper_post_to_forum($forum, $teacher);
$reply = $this->helper_post_to_discussion($forum, $discussion, $teacher, [
'privatereplyto' => $student->id,
]);
[$otherdiscussion, $otherpost] = $this->helper_post_to_forum($forum, $teacher);
// The user is the author.
$entities = array_values($this->vault->get_from_discussion_ids($teacher, [$discussion->id, $otherdiscussion->id], true));
$this->assertCount(3, $entities);
$this->assertEquals($post->id, $entities[0]->get_id());
$this->assertEquals($reply->id, $entities[1]->get_id());
$this->assertEquals($otherpost->id, $entities[2]->get_id());
// The user is the intended recipient.
$entities = array_values($this->vault->get_from_discussion_ids($student, [$discussion->id, $otherdiscussion->id], false));
$this->assertCount(3, $entities);
$this->assertEquals($post->id, $entities[0]->get_id());
$this->assertEquals($reply->id, $entities[1]->get_id());
$this->assertEquals($otherpost->id, $entities[2]->get_id());
// The user is another teacher..
$entities = array_values(
$this->vault->get_from_discussion_ids($otherteacher, [$discussion->id, $otherdiscussion->id], true));
$this->assertCount(3, $entities);
$this->assertEquals($post->id, $entities[0]->get_id());
$this->assertEquals($reply->id, $entities[1]->get_id());
$this->assertEquals($otherpost->id, $entities[2]->get_id());
// The user is a different student.
$entities = array_values(
$this->vault->get_from_discussion_ids($otherstudent, [$discussion->id, $otherdiscussion->id], false));
$this->assertCount(2, $entities);
$this->assertEquals($post->id, $entities[0]->get_id());
$this->assertEquals($otherpost->id, $entities[1]->get_id());
}
/**
* Test get_replies_to_post.
*
@@ -191,20 +290,181 @@ class mod_forum_vaults_post_testcase extends advanced_testcase {
$post3 = $entityfactory->get_post_from_stdclass($post3);
$post4 = $entityfactory->get_post_from_stdclass($post4);
$entities = $this->vault->get_replies_to_post($post1);
$entities = $this->vault->get_replies_to_post($user, $post1, false);
$this->assertCount(3, $entities);
$this->assertEquals($post2->get_id(), $entities[0]->get_id());
$this->assertEquals($post3->get_id(), $entities[1]->get_id());
$this->assertEquals($post4->get_id(), $entities[2]->get_id());
$entities = $this->vault->get_replies_to_post($post2);
$entities = $this->vault->get_replies_to_post($user, $post2, false);
$this->assertCount(1, $entities);
$this->assertEquals($post4->get_id(), $entities[0]->get_id());
$entities = $this->vault->get_replies_to_post($post3);
$entities = $this->vault->get_replies_to_post($user, $post3, false);
$this->assertCount(0, $entities);
}
/**
* Test get_replies_to_post with private replies.
*
* @covers ::get_replies_to_post
* @covers ::<!public>
*/
public function test_get_replies_to_post_private_replies() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
]);
// Generate a structure:
// Initial post p [student]
// -> Reply pa [otherstudent]
// ---> Reply paa [student]
// ---> Private Reply pab [teacher]
// -> Private Reply pb [teacher]
// -> Reply pc [otherstudent]
// ---> Reply pca [student]
// -----> Reply pcaa [otherstudent]
// -------> Private Reply pcaaa [teacher].
[$student, $otherstudent] = $this->helper_create_users($course, 2, 'student');
[$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher');
[$discussion, $p] = $this->helper_post_to_forum($forum, $student);
$pa = $this->helper_reply_to_post($p, $otherstudent);
$paa = $this->helper_reply_to_post($pa, $student);
$pab = $this->helper_reply_to_post($pa, $teacher, ['privatereplyto' => $otherstudent->id]);
$pb = $this->helper_reply_to_post($p, $teacher, ['privatereplyto' => $student->id]);
$pc = $this->helper_reply_to_post($p, $otherteacher);
$pca = $this->helper_reply_to_post($pc, $student);
$pcaa = $this->helper_reply_to_post($pca, $otherstudent);
$pcaaa = $this->helper_reply_to_post($pcaa, $teacher, ['privatereplyto' => $otherstudent->id]);
$entityfactory = \mod_forum\local\container::get_entity_factory();
$ep = $entityfactory->get_post_from_stdclass($p);
$epa = $entityfactory->get_post_from_stdclass($pa);
$epaa = $entityfactory->get_post_from_stdclass($paa);
$epab = $entityfactory->get_post_from_stdclass($pab);
$epb = $entityfactory->get_post_from_stdclass($pb);
$epc = $entityfactory->get_post_from_stdclass($pc);
$epca = $entityfactory->get_post_from_stdclass($pca);
$epcaa = $entityfactory->get_post_from_stdclass($pcaa);
$epcaaa = $entityfactory->get_post_from_stdclass($pcaaa);
// As `student`, you should see all public posts, plus all private replies intended for you.
$entities = $this->vault->get_replies_to_post($student, $ep, false);
$this->assertCount(6, $entities);
$this->assertEquals($epa->get_id(), $entities[0]->get_id());
$this->assertEquals($epaa->get_id(), $entities[1]->get_id());
$this->assertEquals($epb->get_id(), $entities[2]->get_id());
$this->assertEquals($epc->get_id(), $entities[3]->get_id());
$this->assertEquals($epca->get_id(), $entities[4]->get_id());
$this->assertEquals($epcaa->get_id(), $entities[5]->get_id());
$entities = $this->vault->get_replies_to_post($student, $epa, false);
$this->assertCount(1, $entities);
$this->assertEquals($epaa->get_id(), $entities[0]->get_id());
$this->assertEmpty($this->vault->get_replies_to_post($student, $epaa, false));
$this->assertEmpty($this->vault->get_replies_to_post($student, $epab, false));
$this->assertEmpty($this->vault->get_replies_to_post($student, $epb, false));
$this->assertEmpty($this->vault->get_replies_to_post($student, $epcaa, false));
$this->assertEmpty($this->vault->get_replies_to_post($student, $epcaaa, false));
$entities = $this->vault->get_replies_to_post($student, $epc, false);
$this->assertCount(2, $entities);
$this->assertEquals($epca->get_id(), $entities[0]->get_id());
$this->assertEquals($epcaa->get_id(), $entities[1]->get_id());
// As `otherstudent`, you should see all public posts, plus all private replies intended for you.
$entities = $this->vault->get_replies_to_post($otherstudent, $ep, false);
$this->assertCount(7, $entities);
$this->assertEquals($epa->get_id(), $entities[0]->get_id());
$this->assertEquals($epaa->get_id(), $entities[1]->get_id());
$this->assertEquals($epab->get_id(), $entities[2]->get_id());
$this->assertEquals($epc->get_id(), $entities[3]->get_id());
$this->assertEquals($epca->get_id(), $entities[4]->get_id());
$this->assertEquals($epcaa->get_id(), $entities[5]->get_id());
$this->assertEquals($epcaaa->get_id(), $entities[6]->get_id());
$entities = $this->vault->get_replies_to_post($otherstudent, $epa, false);
$this->assertCount(2, $entities);
$this->assertEquals($epaa->get_id(), $entities[0]->get_id());
$this->assertEquals($epab->get_id(), $entities[1]->get_id());
$this->assertEmpty($this->vault->get_replies_to_post($otherstudent, $epaa, false));
$this->assertEmpty($this->vault->get_replies_to_post($otherstudent, $epab, false));
$this->assertEmpty($this->vault->get_replies_to_post($otherstudent, $epb, false));
$this->assertEmpty($this->vault->get_replies_to_post($otherstudent, $epcaaa, false));
$entities = $this->vault->get_replies_to_post($otherstudent, $epc, false);
$this->assertCount(3, $entities);
$this->assertEquals($epca->get_id(), $entities[0]->get_id());
$this->assertEquals($epcaa->get_id(), $entities[1]->get_id());
$this->assertEquals($epcaaa->get_id(), $entities[2]->get_id());
// The teacher who authored the private replies can see all.
$entities = $this->vault->get_replies_to_post($teacher, $ep, true);
$this->assertCount(8, $entities);
$this->assertEquals($epa->get_id(), $entities[0]->get_id());
$this->assertEquals($epaa->get_id(), $entities[1]->get_id());
$this->assertEquals($epab->get_id(), $entities[2]->get_id());
$this->assertEquals($epb->get_id(), $entities[3]->get_id());
$this->assertEquals($epc->get_id(), $entities[4]->get_id());
$this->assertEquals($epca->get_id(), $entities[5]->get_id());
$this->assertEquals($epcaa->get_id(), $entities[6]->get_id());
$this->assertEquals($epcaaa->get_id(), $entities[7]->get_id());
$entities = $this->vault->get_replies_to_post($teacher, $epa, true);
$this->assertCount(2, $entities);
$this->assertEquals($epaa->get_id(), $entities[0]->get_id());
$this->assertEquals($epab->get_id(), $entities[1]->get_id());
$this->assertEmpty($this->vault->get_replies_to_post($teacher, $epaa, true));
$this->assertEmpty($this->vault->get_replies_to_post($teacher, $epab, true));
$this->assertEmpty($this->vault->get_replies_to_post($teacher, $epb, true));
$this->assertEmpty($this->vault->get_replies_to_post($teacher, $epcaaa, true));
$entities = $this->vault->get_replies_to_post($teacher, $epc, true);
$this->assertCount(3, $entities);
$this->assertEquals($epca->get_id(), $entities[0]->get_id());
$this->assertEquals($epcaa->get_id(), $entities[1]->get_id());
$this->assertEquals($epcaaa->get_id(), $entities[2]->get_id());
// Any other teacher can also see all.
$entities = $this->vault->get_replies_to_post($otherteacher, $ep, true);
$this->assertCount(8, $entities);
$this->assertEquals($epa->get_id(), $entities[0]->get_id());
$this->assertEquals($epaa->get_id(), $entities[1]->get_id());
$this->assertEquals($epab->get_id(), $entities[2]->get_id());
$this->assertEquals($epb->get_id(), $entities[3]->get_id());
$this->assertEquals($epc->get_id(), $entities[4]->get_id());
$this->assertEquals($epca->get_id(), $entities[5]->get_id());
$this->assertEquals($epcaa->get_id(), $entities[6]->get_id());
$this->assertEquals($epcaaa->get_id(), $entities[7]->get_id());
$entities = $this->vault->get_replies_to_post($otherteacher, $epa, true);
$this->assertCount(2, $entities);
$this->assertEquals($epaa->get_id(), $entities[0]->get_id());
$this->assertEquals($epab->get_id(), $entities[1]->get_id());
$this->assertEmpty($this->vault->get_replies_to_post($otherteacher, $epaa, true));
$this->assertEmpty($this->vault->get_replies_to_post($otherteacher, $epab, true));
$this->assertEmpty($this->vault->get_replies_to_post($otherteacher, $epb, true));
$this->assertEmpty($this->vault->get_replies_to_post($otherteacher, $epcaaa, true));
$entities = $this->vault->get_replies_to_post($otherteacher, $epc, true);
$this->assertCount(3, $entities);
$this->assertEquals($epca->get_id(), $entities[0]->get_id());
$this->assertEquals($epcaa->get_id(), $entities[1]->get_id());
$this->assertEquals($epcaaa->get_id(), $entities[2]->get_id());
}
/**
* Test get_reply_count_for_discussion_ids when no discussion ids were provided.
*
@@ -218,8 +478,7 @@ class mod_forum_vaults_post_testcase extends advanced_testcase {
$course = $datagenerator->create_course();
$forum = $datagenerator->create_module('forum', ['course' => $course->id]);
$counts = $this->vault->get_reply_count_for_discussion_ids([]);
$this->assertCount(0, $counts);
$this->assertCount(0, $this->vault->get_reply_count_for_discussion_ids($user, [], false));
}
/**
@@ -243,31 +502,168 @@ class mod_forum_vaults_post_testcase extends advanced_testcase {
$post6 = $this->helper_reply_to_post($post5, $user);
[$discussion3, $post7] = $this->helper_post_to_forum($forum, $user);
$counts = $this->vault->get_reply_count_for_discussion_ids([$discussion1->id]);
$counts = $this->vault->get_reply_count_for_discussion_ids($user, [$discussion1->id], false);
$this->assertCount(1, $counts);
$this->assertEquals(3, $counts[$discussion1->id]);
$counts = $this->vault->get_reply_count_for_discussion_ids([$discussion1->id, $discussion2->id]);
$counts = $this->vault->get_reply_count_for_discussion_ids($user, [$discussion1->id, $discussion2->id], false);
$this->assertCount(2, $counts);
$this->assertEquals(3, $counts[$discussion1->id]);
$this->assertEquals(1, $counts[$discussion2->id]);
$counts = $this->vault->get_reply_count_for_discussion_ids([$discussion1->id, $discussion2->id, $discussion3->id]);
$counts = $this->vault->get_reply_count_for_discussion_ids($user, [
$discussion1->id,
$discussion2->id,
$discussion3->id
], false);
$this->assertCount(2, $counts);
$this->assertEquals(3, $counts[$discussion1->id]);
$this->assertEquals(1, $counts[$discussion2->id]);
$counts = $this->vault->get_reply_count_for_discussion_ids([
$counts = $this->vault->get_reply_count_for_discussion_ids($user, [
$discussion1->id,
$discussion2->id,
$discussion3->id,
$discussion3->id + 1000
]);
], false);
$this->assertCount(2, $counts);
$this->assertEquals(3, $counts[$discussion1->id]);
$this->assertEquals(1, $counts[$discussion2->id]);
}
/**
* Test get_reply_count_for_discussion_ids.
*
* @covers ::get_reply_count_for_discussion_ids
* @covers ::<!public>
*/
public function test_get_reply_count_for_discussion_ids_private_replies() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
]);
// Generate a structure:
// Initial post p [student]
// -> Reply pa [otherstudent]
// ---> Reply paa [student]
// ---> Private Reply pab [teacher]
// -> Private Reply pb [teacher]
// -> Reply pc [otherstudent]
// ---> Reply pca [student]
// -----> Reply pcaa [otherstudent]
// -------> Private Reply pcaaa [teacher].
[$student, $otherstudent] = $this->helper_create_users($course, 2, 'student');
[$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher');
[$discussion, $p] = $this->helper_post_to_forum($forum, $student);
$pa = $this->helper_reply_to_post($p, $otherstudent);
$paa = $this->helper_reply_to_post($pa, $student);
$pab = $this->helper_reply_to_post($pa, $teacher, ['privatereplyto' => $otherstudent->id]);
$pb = $this->helper_reply_to_post($p, $teacher, ['privatereplyto' => $student->id]);
$pc = $this->helper_reply_to_post($p, $otherteacher);
$pca = $this->helper_reply_to_post($pc, $student);
$pcaa = $this->helper_reply_to_post($pca, $otherstudent);
$pcaaa = $this->helper_reply_to_post($pcaa, $teacher, ['privatereplyto' => $otherstudent->id]);
$this->assertEquals([$discussion->id => 6],
$this->vault->get_reply_count_for_discussion_ids($student, [$discussion->id], false));
$this->assertEquals([$discussion->id => 7],
$this->vault->get_reply_count_for_discussion_ids($otherstudent, [$discussion->id], false));
$this->assertEquals([$discussion->id => 8],
$this->vault->get_reply_count_for_discussion_ids($teacher, [$discussion->id], true));
$this->assertEquals([$discussion->id => 8],
$this->vault->get_reply_count_for_discussion_ids($otherteacher, [$discussion->id], true));
}
/**
* Test get_reply_count_for_discussion_id.
*
* @covers ::get_reply_count_for_post_id_in_discussion_id
* @covers ::<!public>
*/
public function test_get_reply_count_for_post_id_in_discussion_id() {
$this->resetAfterTest();
$datagenerator = $this->getDataGenerator();
$user = $datagenerator->create_user();
$course = $datagenerator->create_course();
$forum = $datagenerator->create_module('forum', ['course' => $course->id]);
[$discussion1, $post1] = $this->helper_post_to_forum($forum, $user);
$post2 = $this->helper_reply_to_post($post1, $user);
$post3 = $this->helper_reply_to_post($post1, $user);
$post4 = $this->helper_reply_to_post($post2, $user);
[$discussion2, $post5] = $this->helper_post_to_forum($forum, $user);
$post6 = $this->helper_reply_to_post($post5, $user);
[$discussion3, $post7] = $this->helper_post_to_forum($forum, $user);
$this->assertEquals(3,
$this->vault->get_reply_count_for_post_id_in_discussion_id($user, $post1->id, $discussion1->id, false));
$this->assertEquals(1,
$this->vault->get_reply_count_for_post_id_in_discussion_id($user, $post5->id, $discussion2->id, false));
$this->assertEquals(0,
$this->vault->get_reply_count_for_post_id_in_discussion_id($user, $post7->id, $discussion3->id, false));
$this->assertEquals(0,
$this->vault->get_reply_count_for_post_id_in_discussion_id($user, $post7->id + 1000, $discussion3->id, false));
}
/**
* Test get_reply_count_for_post_id_in_discussion_id.
*
* @covers ::get_reply_count_for_post_id_in_discussion_id
* @covers ::<!public>
*/
public function test_get_reply_count_for_post_id_in_discussion_id_private_replies() {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', [
'course' => $course->id,
]);
// Generate a structure:
// Initial post p [student]
// -> Reply pa [otherstudent]
// ---> Reply paa [student]
// ---> Private Reply pab [teacher]
// -> Private Reply pb [teacher]
// -> Reply pc [otherstudent]
// ---> Reply pca [student]
// -----> Reply pcaa [otherstudent]
// -------> Private Reply pcaaa [teacher].
[$student, $otherstudent] = $this->helper_create_users($course, 2, 'student');
[$teacher, $otherteacher] = $this->helper_create_users($course, 2, 'teacher');
[$discussion, $p] = $this->helper_post_to_forum($forum, $student);
$pa = $this->helper_reply_to_post($p, $otherstudent);
$paa = $this->helper_reply_to_post($pa, $student);
$pab = $this->helper_reply_to_post($pa, $teacher, ['privatereplyto' => $otherstudent->id]);
$pb = $this->helper_reply_to_post($p, $teacher, ['privatereplyto' => $student->id]);
$pc = $this->helper_reply_to_post($p, $otherteacher);
$pca = $this->helper_reply_to_post($pc, $student);
$pcaa = $this->helper_reply_to_post($pca, $otherstudent);
$pcaaa = $this->helper_reply_to_post($pcaa, $teacher, ['privatereplyto' => $otherstudent->id]);
$this->assertEquals(6,
$this->vault->get_reply_count_for_post_id_in_discussion_id($student, $p->id, $discussion->id, false));
$this->assertEquals(7,
$this->vault->get_reply_count_for_post_id_in_discussion_id($otherstudent, $p->id, $discussion->id, false));
$this->assertEquals(8,
$this->vault->get_reply_count_for_post_id_in_discussion_id($teacher, $p->id, $discussion->id, true));
$this->assertEquals(8,
$this->vault->get_reply_count_for_post_id_in_discussion_id($otherteacher, $p->id, $discussion->id, true));
}
/**
* Test get_unread_count_for_discussion_ids.
*
@@ -305,11 +701,11 @@ class mod_forum_vaults_post_testcase extends advanced_testcase {
forum_tp_add_read_record($user->id, $post4->id);
$CFG->forum_oldpostdays = 1;
$counts = $this->vault->get_unread_count_for_discussion_ids($user, [$discussion1->id]);
$counts = $this->vault->get_unread_count_for_discussion_ids($user, [$discussion1->id], false);
$this->assertCount(1, $counts);
$this->assertEquals(2, $counts[$discussion1->id]);
$counts = $this->vault->get_unread_count_for_discussion_ids($user, [$discussion1->id, $discussion2->id]);
$counts = $this->vault->get_unread_count_for_discussion_ids($user, [$discussion1->id, $discussion2->id], false);
$this->assertCount(2, $counts);
$this->assertEquals(2, $counts[$discussion1->id]);
$this->assertEquals(2, $counts[$discussion2->id]);
@@ -318,12 +714,12 @@ class mod_forum_vaults_post_testcase extends advanced_testcase {
$discussion1->id,
$discussion2->id,
$discussion2->id + 1000
]);
], false);
$this->assertCount(2, $counts);
$this->assertEquals(2, $counts[$discussion1->id]);
$this->assertEquals(2, $counts[$discussion2->id]);
$counts = $this->vault->get_unread_count_for_discussion_ids($otheruser, [$discussion1->id, $discussion2->id]);
$counts = $this->vault->get_unread_count_for_discussion_ids($otheruser, [$discussion1->id, $discussion2->id], false);
$this->assertCount(2, $counts);
$this->assertEquals(4, $counts[$discussion1->id]);
$this->assertEquals(2, $counts[$discussion2->id]);
@@ -366,27 +762,29 @@ class mod_forum_vaults_post_testcase extends advanced_testcase {
$post6 = $this->helper_reply_to_post($post5, $user);
[$discussion3, $post7] = $this->helper_post_to_forum($forum, $user);
$ids = $this->vault->get_latest_post_id_for_discussion_ids([$discussion1->id]);
$ids = $this->vault->get_latest_post_id_for_discussion_ids($user, [$discussion1->id], false);
$this->assertCount(1, $ids);
$this->assertEquals($post4->id, $ids[$discussion1->id]);
$ids = $this->vault->get_latest_post_id_for_discussion_ids([$discussion1->id, $discussion2->id]);
$ids = $this->vault->get_latest_post_id_for_discussion_ids($user,
[$discussion1->id, $discussion2->id], false);
$this->assertCount(2, $ids);
$this->assertEquals($post4->id, $ids[$discussion1->id]);
$this->assertEquals($post6->id, $ids[$discussion2->id]);
$ids = $this->vault->get_latest_post_id_for_discussion_ids([$discussion1->id, $discussion2->id, $discussion3->id]);
$ids = $this->vault->get_latest_post_id_for_discussion_ids($user,
[$discussion1->id, $discussion2->id, $discussion3->id], false);
$this->assertCount(3, $ids);
$this->assertEquals($post4->id, $ids[$discussion1->id]);
$this->assertEquals($post6->id, $ids[$discussion2->id]);
$this->assertEquals($post7->id, $ids[$discussion3->id]);
$ids = $this->vault->get_latest_post_id_for_discussion_ids([
$ids = $this->vault->get_latest_post_id_for_discussion_ids($user, [
$discussion1->id,
$discussion2->id,
$discussion3->id,
$discussion3->id + 1000
]);
], false);
$this->assertCount(3, $ids);
$this->assertEquals($post4->id, $ids[$discussion1->id]);
$this->assertEquals($post6->id, $ids[$discussion2->id]);
@@ -407,6 +805,6 @@ class mod_forum_vaults_post_testcase extends advanced_testcase {
$course = $datagenerator->create_course();
$forum = $datagenerator->create_module('forum', ['course' => $course->id]);
$this->assertEquals([], $this->vault->get_latest_post_id_for_discussion_ids([]));
$this->assertEquals([], $this->vault->get_latest_post_id_for_discussion_ids($user, [], false));
}
}
+4
View File
@@ -6,8 +6,12 @@ information provided here is intended especially for developers.
* Added new forum entities, factories, exporters, renderers, and vaults in the local namespace to better encapsulate the forum data.
* Deprecated all of the forum_print_* functions in lib.php.
* The forum_print_latest_discussions function has been deprecated and will not be replaced.
* The get_forum_discussion_posts web service has been deprecated in favour of get_discussion_posts.
* The forum_count_replies function has been deprecated in favour of get_reply_count_for_post_id_in_discussion_id in
the Post vault.
=== 3.6 ===
* forum_print_post should be surrounded with calls to forum_print_post_start and forum_print_post_end to create the proper HTML structure for the post.
=== 3.4 ===
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2018120302; // The current module version (Date: YYYYMMDDXX)
$plugin->version = 2019031300; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2018112800; // Requires this Moodle version
$plugin->component = 'mod_forum'; // Full name of the plugin (used for diagnostics)
+6 -1
View File
@@ -146,7 +146,12 @@ switch ($forum->get_type()) {
$hasmultiplediscussions, $displaymode);
$post = $postvault->get_from_id($discussion->get_first_post_id());
$orderpostsby = $displaymode == FORUM_MODE_FLATNEWEST ? 'created DESC' : 'created ASC';
$replies = $postvault->get_replies_to_post($post, $orderpostsby);
$replies = $postvault->get_replies_to_post(
$USER,
$post,
$capabilitymanager->can_view_any_private_reply($USER),
$orderpostsby
);
echo $discussionsrenderer->render($USER, $post, $replies);
break;
case 'blog':