MDL-22077 forum: Add forum private replies

This patch adds new capabilities:
'mod/forum:postprivatereply'   - whether a user is able to post private replies; and
'mod/forum:readprivatereplies' - whether a user is able to read private replies.

Private replies are only visible to the intended recipient (the author of
the parent post), the author of the private reply, and those with the
ability to read private replies.

If a post is private then it cannot be replied to further.
This commit is contained in:
Andrew Nicols
2019-04-04 11:25:29 +08:00
committed by Peter
parent 12a1f9c567
commit bc4c7337e1
47 changed files with 1805 additions and 141 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;
@@ -84,12 +88,14 @@ class exported_discussion_summaries {
legacy_data_mapper_factory $legacydatamapperfactory,
exporter_factory $exporterfactory,
vault_factory $vaultfactory,
manager_factory $managerfactory,
rating_manager $ratingmanager
) {
$this->renderer = $renderer;
$this->legacydatamapperfactory = $legacydatamapperfactory;
$this->exporterfactory = $exporterfactory;
$this->vaultfactory = $vaultfactory;
$this->managerfactory = $managerfactory;
$this->ratingmanager = $ratingmanager;
}
@@ -108,16 +114,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 +133,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,
+110
View File
@@ -0,0 +1,110 @@
<?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 mod_forum\local\entities\author as author_entity;
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,6 +105,7 @@ class builder {
$this->legacydatamapperfactory,
$this->exporterfactory,
$this->vaultfactory,
$this->managerfactory,
$this->managerfactory->get_rating_manager()
);
}
+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 (empty($post->get_private_reply_recipient_id())) {
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?
*
+111 -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,29 @@ 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));
}
/**
@@ -215,20 +270,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 +297,18 @@ 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 +318,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 +332,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 shwo 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);
+14
View File
@@ -620,6 +620,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 +630,7 @@ class provider implements
$params = [
'postuserid' => $userid,
'readuserid' => $userid,
'privatereplyrecipient' => $userid,
];
$params += $forumparams;
$params += $ratingsql->params;
@@ -666,11 +668,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 +694,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 +766,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;
}
+1 -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').')';
+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)));
+118 -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.
*
@@ -267,6 +364,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 +463,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 +473,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 +577,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 +881,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 +917,7 @@ class mod_forum_external extends external_api {
'options' => $options
)
);
$warnings = array();
if (!$parent = forum_get_post_full($params['postid'])) {
@@ -826,6 +938,7 @@ class mod_forum_external extends external_api {
// Validate options.
$options = array(
'discussionsubscribe' => true,
'private' => false,
'inlineattachmentsid' => 0,
'attachmentsid' => null
);
@@ -835,6 +948,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 +984,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)) {
+5
View File
@@ -248,6 +248,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 +408,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}';
@@ -487,6 +490,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 -22
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;
@@ -3989,6 +4018,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 +6574,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);
}
+35 -13
View File
@@ -98,6 +98,8 @@ 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();
$isprivatereply = false;
$canreplyprivately = false;
if (!empty($forum)) { // User is starting a new discussion in a forum.
if (! $forum = $DB->get_record("forum", array("id" => $forum))) {
@@ -215,6 +217,10 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
print_error("activityiscurrentlyhidden");
}
if (!empty($parent->privatereplyto)) {
print_error('cannotreplytoprivatereply', 'forum');
}
// Load up the $post variable.
$post = new stdClass();
@@ -225,6 +231,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 = forum_user_can_reply_privately($modcontext, $parent);
$post->groupid = ($discussion->groupid == -1) ? 0 : $discussion->groupid;
@@ -275,12 +283,14 @@ 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 ($parent) {
$canreplyprivately = forum_user_can_reply_privately($modcontext, $parent);
}
$post = trusttext_pre_edit($post, 'message', $modcontext);
@@ -411,7 +421,12 @@ if (!empty($forum)) { // User is starting a new discussion in a forum.
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,
$capabilitymanager->can_view_any_private_reply($USER),
'created ASC'
);
$postentities = array_merge($postentities, $replies);
}
@@ -591,16 +606,18 @@ if (!isset($forum->maxattachments)) { // TODO - delete this once we add a field
}
$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;
@@ -1074,7 +1091,12 @@ if (!empty($parent)) {
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');
$replies = $postvault->get_replies_to_post(
$USER,
$postentity,
$capabilitymanager->can_view_any_private_reply($USER),
'created ASC'
);
$postentities = array_merge($postentities, $replies);
}
}
+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
);
+96
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).
@@ -1301,4 +1303,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,
],
]);
}
}
+4
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.
+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]);
}
}
+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);
+87
View File
@@ -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 <[email protected]>
* @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 <[email protected]>
* @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));
}
}
+284 -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,26 +502,26 @@ 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]);
@@ -305,11 +564,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 +577,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 +625,27 @@ 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 +666,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));
}
}
+2
View File
@@ -6,8 +6,10 @@ 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.
=== 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':