Merge branch 'MDL-46881-master' of git://github.com/andrewnicols/moodle

This commit is contained in:
Eloy Lafuente (stronk7)
2019-01-30 11:12:21 +01:00
17 changed files with 3431 additions and 1696 deletions
+112
View File
@@ -0,0 +1,112 @@
<?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/>.
/**
* This file defines a trait to assist with logging in tasks.
*
* @package core
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\task;
defined('MOODLE_INTERNAL') || die();
/**
* This trait includes functions to assist with logging in tasks.
*
* @package core
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait logging_trait {
/**
* @var \progress_trace
*/
protected $trace = null;
/**
* @var \stdClass
*/
protected $tracestats = null;
/**
* Get the progress_trace.
*
* @return \progress_trace
*/
protected function get_trace() {
if (null === $this->trace) {
$this->trace = new \text_progress_trace();
$this->tracestats = new \stdClass();
}
return $this->trace;
}
/**
* Log a message to the progress tracer.
*
* @param string $message
* @param int $depth
*/
protected function log($message, $depth = 1) {
$this->get_trace()
->output($message, $depth);
}
/**
* Log a start message to the progress tracer.
*
* @param string $message
* @param int $depth
*/
protected function log_start($message, $depth = 0) {
$this->log($message, $depth);
if (defined('MDL_PERFTOLOG') && MDL_PERFTOLOG) {
$this->tracestats->$depth = [
'mem' => memory_get_usage(),
'time' => microtime(),
];
}
}
/**
* Log an end message to the progress tracer.
*
* @param string $message
* @param int $depth
*/
protected function log_finish($message, $depth = 0) {
$this->log($message, $depth);
if (isset($this->tracestats->$depth)) {
$startstats = $this->tracestats->$depth;
$this->log(
sprintf("Time taken %s, memory total: %s, Memory growth: %s, Memory peak: %s",
microtime_diff($startstats['time'], microtime()),
display_size(memory_get_usage()),
display_size(memory_get_usage() - $startstats['mem']),
display_size(memory_get_peak_usage())
),
$depth + 1
);
}
}
}
+37 -2
View File
@@ -129,7 +129,18 @@ class manager {
* @return bool
*/
protected static function task_is_scheduled($task) {
return false !== self::get_queued_adhoc_task_record($task);
}
/**
* Checks if the task with the same classname, component and customdata is already scheduled
*
* @param adhoc_task $task
* @return bool
*/
protected static function get_queued_adhoc_task_record($task) {
global $DB;
$record = self::record_from_adhoc_task($task);
$params = [$record->classname, $record->component, $record->customdata];
$sql = 'classname = ? AND component = ? AND ' .
@@ -139,14 +150,38 @@ class manager {
$params[] = $record->userid;
$sql .= " AND userid = ? ";
}
return $DB->record_exists_select('task_adhoc', $sql, $params);
return $DB->get_record_select('task_adhoc', $sql, $params);
}
/**
* Schedule a new task, or reschedule an existing adhoc task which has matching data.
*
* Only a task matching the same user, classname, component, and customdata will be rescheduled.
* If these values do not match exactly then a new task is scheduled.
*
* @param \core\task\adhoc_task $task - The new adhoc task information to store.
* @since Moodle 3.7
*/
public static function reschedule_or_queue_adhoc_task(adhoc_task $task) : void {
global $DB;
if ($existingrecord = self::get_queued_adhoc_task_record($task)) {
// Only update the next run time if it is explicitly set on the task.
$nextruntime = $task->get_next_run_time();
if ($nextruntime && ($existingrecord->nextruntime != $nextruntime)) {
$DB->set_field('task_adhoc', 'nextruntime', $nextruntime, ['id' => $existingrecord->id]);
}
} else {
// There is nothing queued yet. Just queue as normal.
self::queue_adhoc_task($task);
}
}
/**
* Queue an adhoc task to run in the background.
*
* @param \core\task\adhoc_task $task - The new adhoc task information to store.
* @param bool $checkforexisting - If set to true and the task with the same classname, component and customdata
* @param bool $checkforexisting - If set to true and the task with the same user, classname, component and customdata
* is already scheduled then it will not schedule a new task. Can be used only for ASAP tasks.
* @return boolean - True if the config was saved.
*/
+108
View File
@@ -150,6 +150,114 @@ class core_adhoc_task_testcase extends advanced_testcase {
}
}
/**
* Ensure that the reschedule_or_queue_adhoc_task function will schedule a new task if no tasks exist.
*/
public function test_reschedule_or_queue_adhoc_task_no_existing() {
$this->resetAfterTest(true);
// Schedule adhoc task.
$task = new \core\task\adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
\core\task\manager::reschedule_or_queue_adhoc_task($task);
$this->assertEquals(1, count(\core\task\manager::get_adhoc_tasks('core\task\adhoc_test_task')));
}
/**
* Ensure that the reschedule_or_queue_adhoc_task function will schedule a new task if a task for the same user does
* not exist.
*/
public function test_reschedule_or_queue_adhoc_task_different_user() {
$this->resetAfterTest(true);
$user = \core_user::get_user_by_username('admin');
// Schedule adhoc task.
$task = new \core\task\adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
\core\task\manager::reschedule_or_queue_adhoc_task($task);
// Schedule adhoc task for a different user.
$task = new \core\task\adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
$task->set_userid($user->id);
\core\task\manager::reschedule_or_queue_adhoc_task($task);
$this->assertEquals(2, count(\core\task\manager::get_adhoc_tasks('core\task\adhoc_test_task')));
}
/**
* Ensure that the reschedule_or_queue_adhoc_task function will schedule a new task if a task with different custom
* data exists.
*/
public function test_reschedule_or_queue_adhoc_task_different_data() {
$this->resetAfterTest(true);
// Schedule adhoc task.
$task = new \core\task\adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
\core\task\manager::reschedule_or_queue_adhoc_task($task);
// Schedule adhoc task for a different user.
$task = new \core\task\adhoc_test_task();
$task->set_custom_data(['courseid' => 11]);
\core\task\manager::reschedule_or_queue_adhoc_task($task);
$this->assertEquals(2, count(\core\task\manager::get_adhoc_tasks('core\task\adhoc_test_task')));
}
/**
* Ensure that the reschedule_or_queue_adhoc_task function will not make any change for matching data if no time was
* specified.
*/
public function test_reschedule_or_queue_adhoc_task_match_no_change() {
$this->resetAfterTest(true);
// Schedule adhoc task.
$task = new \core\task\adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
$task->set_next_run_time(time() + DAYSECS);
\core\task\manager::reschedule_or_queue_adhoc_task($task);
$before = \core\task\manager::get_adhoc_tasks('core\task\adhoc_test_task');
// Schedule the task again but do not specify a time.
$task = new \core\task\adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
\core\task\manager::reschedule_or_queue_adhoc_task($task);
$this->assertEquals(1, count(\core\task\manager::get_adhoc_tasks('core\task\adhoc_test_task')));
$this->assertEquals($before, \core\task\manager::get_adhoc_tasks('core\task\adhoc_test_task'));
}
/**
* Ensure that the reschedule_or_queue_adhoc_task function will update the run time if there are planned changes.
*/
public function test_reschedule_or_queue_adhoc_task_match_update_runtime() {
$this->resetAfterTest(true);
$initialruntime = time() + DAYSECS;
$newruntime = time() + WEEKSECS;
// Schedule adhoc task.
$task = new \core\task\adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
$task->set_next_run_time($initialruntime);
\core\task\manager::reschedule_or_queue_adhoc_task($task);
$before = \core\task\manager::get_adhoc_tasks('core\task\adhoc_test_task');
// Schedule the task again.
$task = new \core\task\adhoc_test_task();
$task->set_custom_data(['courseid' => 10]);
$task->set_next_run_time($newruntime);
\core\task\manager::reschedule_or_queue_adhoc_task($task);
$tasks = \core\task\manager::get_adhoc_tasks('core\task\adhoc_test_task');
$this->assertEquals(1, count($tasks));
$this->assertNotEquals($before, $tasks);
$firsttask = reset($tasks);
$this->assertEquals($newruntime, $firsttask->get_next_run_time());
}
/**
* Test queue_adhoc_task "if not scheduled".
*/
+2
View File
@@ -6,6 +6,8 @@ information provided here is intended especially for developers.
* The method core_user::is_real_user() now returns false for userid = 0 parameter
* 'mform1' dependencies (in themes, js...) will stop working because a randomly generated string has been added to the id
attribute on forms to avoid collisions in forms loaded in AJAX requests.
* A new method to allow queueing or rescheduling of an existing scheduled task was added. This allows an existing task
to be updated or queued as required. This new functionality can be found in \core\task\manager::reschedule_or_queue_adhoc_task.
=== 3.6 ===
+496 -6
View File
@@ -17,16 +17,68 @@
/**
* A scheduled task for forum cron.
*
* @todo MDL-44734 This job will be split up properly.
*
* @package mod_forum
* @copyright 2014 Dan Poltawski <dan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\task;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* The main scheduled task for the forum.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cron_task extends \core\task\scheduled_task {
// Use the logging trait to get some nice, juicy, logging.
use \core\task\logging_trait;
/**
* @var The list of courses which contain posts to be sent.
*/
protected $courses = [];
/**
* @var The list of forums which contain posts to be sent.
*/
protected $forums = [];
/**
* @var The list of discussions which contain posts to be sent.
*/
protected $discussions = [];
/**
* @var The list of posts to be sent.
*/
protected $posts = [];
/**
* @var The list of post authors.
*/
protected $users = [];
/**
* @var The list of subscribed users.
*/
protected $subscribedusers = [];
/**
* @var The list of digest users.
*/
protected $digestusers = [];
/**
* @var The list of adhoc data for sending.
*/
protected $adhocdata = [];
/**
* Get a descriptive name for this task (shown to admins).
*
@@ -37,12 +89,450 @@ class cron_task extends \core\task\scheduled_task {
}
/**
* Run forum cron.
* Execute the scheduled task.
*/
public function execute() {
global $CFG;
require_once($CFG->dirroot . '/mod/forum/lib.php');
forum_cron();
global $CFG, $DB;
$timenow = time();
// Delete any really old posts in the digest queue.
$weekago = $timenow - (7 * 24 * 3600);
$this->log_start("Removing old digest records from 7 days ago.");
$DB->delete_records_select('forum_queue', "timemodified < ?", array($weekago));
$this->log_finish("Removed all old digest records.");
$endtime = $timenow - $CFG->maxeditingtime;
$starttime = $endtime - (2 * DAYSECS);
$this->log_start("Fetching unmailed posts.");
if (!$posts = $this->get_unmailed_posts($starttime, $endtime, $timenow)) {
$this->log_finish("No posts found.", 1);
return false;
}
$this->log_finish("Done");
// Process post data and turn into adhoc tasks.
$this->process_post_data($posts);
// Mark posts as read.
list($in, $params) = $DB->get_in_or_equal(array_keys($posts));
$DB->set_field_select('forum_posts', 'mailed', 1, "id {$in}", $params);
}
/**
* Process all posts and convert to appropriated hoc tasks.
*
* @param \stdClass[] $posts
*/
protected function process_post_data($posts) {
$discussionids = [];
$forumids = [];
$courseids = [];
$this->log_start("Processing post information");
$start = microtime(true);
foreach ($posts as $id => $post) {
$discussionids[$post->discussion] = true;
$forumids[$post->forum] = true;
$courseids[$post->course] = true;
$this->add_data_for_post($post);
$this->posts[$id] = $post;
}
$this->log_finish(sprintf("Processed %s posts", count($this->posts)));
if (empty($this->posts)) {
$this->log("No posts found. Returning early.");
return;
}
// Please note, this order is intentional.
// The forum cache makes use of the course.
$this->log_start("Filling caches");
$start = microtime(true);
$this->log_start("Filling course cache", 1);
$this->fill_course_cache(array_keys($courseids));
$this->log_finish("Done", 1);
$this->log_start("Filling forum cache", 1);
$this->fill_forum_cache(array_keys($forumids));
$this->log_finish("Done", 1);
$this->log_start("Filling discussion cache", 1);
$this->fill_discussion_cache(array_keys($discussionids));
$this->log_finish("Done", 1);
$this->log_start("Filling user subscription cache", 1);
$this->fill_user_subscription_cache();
$this->log_finish("Done", 1);
$this->log_start("Filling digest cache", 1);
$this->fill_digest_cache();
$this->log_finish("Done", 1);
$this->log_finish("All caches filled");
$this->log_start("Queueing user tasks.");
$this->queue_user_tasks();
$this->log_finish("All tasks queued.");
}
/**
* Fill the course cache.
*
* @param int[] $courseids
*/
protected function fill_course_cache($courseids) {
global $DB;
list($in, $params) = $DB->get_in_or_equal($courseids);
$this->courses = $DB->get_records_select('course', "id $in", $params);
}
/**
* Fill the forum cache.
*
* @param int[] $forumids
*/
protected function fill_forum_cache($forumids) {
global $DB;
$requiredfields = [
'id',
'course',
'forcesubscribe',
'type',
];
list($in, $params) = $DB->get_in_or_equal($forumids);
$this->forums = $DB->get_records_select('forum', "id $in", $params, '', implode(', ', $requiredfields));
foreach ($this->forums as $id => $forum) {
\mod_forum\subscriptions::fill_subscription_cache($id);
\mod_forum\subscriptions::fill_discussion_subscription_cache($id);
}
}
/**
* Fill the discussion cache.
*
* @param int[] $discussionids
*/
protected function fill_discussion_cache($discussionids) {
global $DB;
if (empty($discussionids)) {
$this->discussion = [];
} else {
$requiredfields = [
'id',
'groupid',
'firstpost',
'timestart',
'timeend',
];
list($in, $params) = $DB->get_in_or_equal($discussionids);
$this->discussions = $DB->get_records_select(
'forum_discussions', "id $in", $params, '', implode(', ', $requiredfields));
}
}
/**
* Fill the cache of user digest preferences.
*/
protected function fill_digest_cache() {
global $DB;
if (empty($this->users)) {
return;
}
// Get the list of forum subscriptions for per-user per-forum maildigest settings.
list($in, $params) = $DB->get_in_or_equal(array_keys($this->users));
$digestspreferences = $DB->get_recordset_select(
'forum_digests', "userid $in", $params, '', 'id, userid, forum, maildigest');
foreach ($digestspreferences as $digestpreference) {
if (!isset($this->digestusers[$digestpreference->forum])) {
$this->digestusers[$digestpreference->forum] = [];
}
$this->digestusers[$digestpreference->forum][$digestpreference->userid] = $digestpreference->maildigest;
}
$digestspreferences->close();
}
/**
* Add dsta for the current forum post to the structure of adhoc data.
*
* @param \stdClass $post
*/
protected function add_data_for_post($post) {
if (!isset($this->adhocdata[$post->course])) {
$this->adhocdata[$post->course] = [];
}
if (!isset($this->adhocdata[$post->course][$post->forum])) {
$this->adhocdata[$post->course][$post->forum] = [];
}
if (!isset($this->adhocdata[$post->course][$post->forum][$post->discussion])) {
$this->adhocdata[$post->course][$post->forum][$post->discussion] = [];
}
$this->adhocdata[$post->course][$post->forum][$post->discussion][$post->id] = $post->id;
}
/**
* Fill the cache of user subscriptions.
*/
protected function fill_user_subscription_cache() {
foreach ($this->forums as $forum) {
$cm = get_fast_modinfo($this->courses[$forum->course])->instances['forum'][$forum->id];
$modcontext = \context_module::instance($cm->id);
$this->subscribedusers[$forum->id] = [];
if ($users = \mod_forum\subscriptions::fetch_subscribed_users($forum, 0, $modcontext, 'u.id, u.maildigest', true)) {
foreach ($users as $user) {
// This user is subscribed to this forum.
$this->subscribedusers[$forum->id][$user->id] = $user->id;
if (!isset($this->users[$user->id])) {
// Store minimal user info.
$this->users[$user->id] = $user;
}
}
// Release memory.
unset($users);
}
}
}
/**
* Queue the user tasks.
*/
protected function queue_user_tasks() {
global $CFG, $DB;
$timenow = time();
$sitetimezone = \core_date::get_server_timezone();
$counts = [
'digests' => 0,
'individuals' => 0,
'users' => 0,
'ignored' => 0,
'messages' => 0,
];
$this->log("Processing " . count($this->users) . " users", 1);
foreach ($this->users as $user) {
$usercounts = [
'digests' => 0,
'messages' => 0,
];
$send = false;
// Setup this user so that the capabilities are cached, and environment matches receiving user.
cron_setup_user($user);
list($individualpostdata, $digestpostdata) = $this->fetch_posts_for_user($user);
if (!empty($digestpostdata)) {
// Insert all of the records for the digest.
$DB->insert_records('forum_queue', $digestpostdata);
$digesttime = usergetmidnight($timenow, $sitetimezone) + ($CFG->digestmailtime * 3600);
$task = new \mod_forum\task\send_user_digests();
$task->set_userid($user->id);
$task->set_component('mod_forum');
$task->set_next_run_time($digesttime);
\core\task\manager::reschedule_or_queue_adhoc_task($task);
$usercounts['digests']++;
$send = true;
}
if (!empty($individualpostdata)) {
$usercounts['messages'] += count($individualpostdata);
$task = new \mod_forum\task\send_user_notifications();
$task->set_userid($user->id);
$task->set_custom_data($individualpostdata);
$task->set_component('mod_forum');
\core\task\manager::queue_adhoc_task($task);
$counts['individuals']++;
$send = true;
}
if ($send) {
$counts['users']++;
$counts['messages'] += $usercounts['messages'];
$counts['digests'] += $usercounts['digests'];
} else {
$counts['ignored']++;
}
$this->log(sprintf("Queued %d digests and %d messages for %s",
$usercounts['digests'],
$usercounts['messages'],
$user->id
), 2);
}
$this->log(
sprintf(
"Queued %d digests, and %d individual tasks for %d post mails. " .
"Unique users: %d (%d ignored)",
$counts['digests'],
$counts['individuals'],
$counts['messages'],
$counts['users'],
$counts['ignored']
), 1);
}
/**
* Fetch posts for this user.
*
* @param \stdClass $user The user to fetch posts for.
*/
protected function fetch_posts_for_user($user) {
// We maintain a mapping of user groups for each forum.
$usergroups = [];
$digeststructure = [];
$poststructure = $this->adhocdata;
$poststosend = [];
foreach ($poststructure as $courseid => $forumids) {
$course = $this->courses[$courseid];
foreach ($forumids as $forumid => $discussionids) {
$forum = $this->forums[$forumid];
$maildigest = forum_get_user_maildigest_bulk($this->digestusers, $user, $forumid);
if (!isset($this->subscribedusers[$forumid][$user->id])) {
// This user has no subscription of any kind to this forum.
// Do not send them any posts at all.
unset($poststructure[$courseid][$forumid]);
continue;
}
$subscriptiontime = \mod_forum\subscriptions::fetch_discussion_subscription($forum->id, $user->id);
$cm = get_fast_modinfo($course)->instances['forum'][$forumid];
foreach ($discussionids as $discussionid => $postids) {
$discussion = $this->discussions[$discussionid];
if (!\mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussionid, $cm)) {
// The user does not subscribe to this forum as a whole, or to this specific discussion.
unset($poststructure[$courseid][$forumid][$discussionid]);
continue;
}
if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {
// This discussion has a groupmode set (SEPARATEGROUPS or VISIBLEGROUPS).
// Check whether the user can view it based on their groups.
if (!isset($usergroups[$forum->id])) {
$usergroups[$forum->id] = groups_get_all_groups($courseid, $user->id, $cm->groupingid);
}
if (!isset($usergroups[$forum->id][$discussion->groupid])) {
// This user is not a member of this group, or the group no longer exists.
$modcontext = \context_module::instance($cm->id);
if (!has_capability('moodle/site:accessallgroups', $modcontext, $user)) {
// This user does not have the accessallgroups and is not a member of the group.
// Do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS.
unset($poststructure[$courseid][$forumid][$discussionid]);
continue;
}
}
}
foreach ($postids as $postid) {
$post = $this->posts[$postid];
if ($subscriptiontime) {
// Skip posts if the user subscribed to the discussion after it was created.
$subscribedafter = isset($subscriptiontime[$post->discussion]);
$subscribedafter = $subscribedafter && ($subscriptiontime[$post->discussion] > $post->created);
if ($subscribedafter) {
// The user subscribed to the discussion/forum after this post was created.
unset($poststructure[$courseid][$forumid][$discussionid][$postid]);
continue;
}
}
if ($maildigest > 0) {
// This user wants the mails to be in digest form.
$digeststructure[] = (object) [
'userid' => $user->id,
'discussionid' => $discussion->id,
'postid' => $post->id,
'timemodified' => $post->created,
];
unset($poststructure[$courseid][$forumid][$discussionid][$postid]);
continue;
} else {
// Add this post to the list of postids to be sent.
$poststosend[] = $postid;
}
}
}
if (empty($poststructure[$courseid][$forumid])) {
// This user is not subscribed to any discussions in this forum at all.
unset($poststructure[$courseid][$forumid]);
continue;
}
}
if (empty($poststructure[$courseid])) {
// This user is not subscribed to any forums in this course.
unset($poststructure[$courseid]);
}
}
return [$poststosend, $digeststructure];
}
/**
* Returns a list of all new posts that have not been mailed yet
*
* @param int $starttime posts created after this time
* @param int $endtime posts created before this
* @param int $now used for timed discussions only
* @return array
*/
protected function get_unmailed_posts($starttime, $endtime, $now = null) {
global $CFG, $DB;
$params = array();
$params['mailed'] = FORUM_MAILED_PENDING;
$params['ptimestart'] = $starttime;
$params['ptimeend'] = $endtime;
$params['mailnow'] = 1;
if (!empty($CFG->forum_enabletimedposts)) {
if (empty($now)) {
$now = time();
}
$selectsql = "AND (p.created >= :ptimestart OR d.timestart >= :pptimestart)";
$params['pptimestart'] = $starttime;
$timedsql = "AND (d.timestart < :dtimestart AND (d.timeend = 0 OR d.timeend > :dtimeend))";
$params['dtimestart'] = $now;
$params['dtimeend'] = $now;
} else {
$timedsql = "";
$selectsql = "AND p.created >= :ptimestart";
}
return $DB->get_records_sql(
"SELECT
p.id,
p.discussion,
d.forum,
d.course,
p.created,
p.parent,
p.userid
FROM {forum_posts} p
JOIN {forum_discussions} d ON d.id = p.discussion
WHERE p.mailed = :mailed
$selectsql
AND (p.created < :ptimeend OR p.mailnow = :mailnow)
$timedsql
ORDER BY p.modified ASC",
$params);
}
}
@@ -0,0 +1,616 @@
<?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/>.
/**
* This file defines an adhoc task to send notifications.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\task;
defined('MOODLE_INTERNAL') || die();
use html_writer;
require_once($CFG->dirroot . '/mod/forum/lib.php');
/**
* Adhoc task to send moodle forum digests for the specified user.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class send_user_digests extends \core\task\adhoc_task {
// Use the logging trait to get some nice, juicy, logging.
use \core\task\logging_trait;
/**
* @var \stdClass A shortcut to $USER.
*/
protected $recipient;
/**
* @var bool[] Whether the user can view fullnames for each forum.
*/
protected $viewfullnames = [];
/**
* @var bool[] Whether the user can post in each forum.
*/
protected $canpostto = [];
/**
* @var \stdClass[] Courses with posts them.
*/
protected $courses = [];
/**
* @var \stdClass[] Forums with posts them.
*/
protected $forums = [];
/**
* @var \stdClass[] Discussions with posts them.
*/
protected $discussions = [];
/**
* @var \stdClass[] The posts to be sent.
*/
protected $posts = [];
/**
* @var \stdClass[] The various authors.
*/
protected $users = [];
/**
* @var \stdClass[] A list of any per-forum digest preference that this user holds.
*/
protected $forumdigesttypes = [];
/**
* @var bool Whether the user has requested HTML or not.
*/
protected $allowhtml = true;
/**
* @var string The subject of the message.
*/
protected $postsubject = '';
/**
* @var string The plaintext content of the whole message.
*/
protected $notificationtext = '';
/**
* @var string The HTML content of the whole message.
*/
protected $notificationhtml = '';
/**
* @var string The plaintext content for the current discussion being processed.
*/
protected $discussiontext = '';
/**
* @var string The HTML content for the current discussion being processed.
*/
protected $discussionhtml = '';
/**
* @var int The number of messages sent in this digest.
*/
protected $sentcount = 0;
/**
* @var \renderer[][] A cache of the different types of renderer, stored both by target (HTML, or Text), and type.
*/
protected $renderers = [
'html' => [],
'text' => [],
];
/**
* @var int[] A list of post IDs to be marked as read for this user.
*/
protected $markpostsasread = [];
/**
* Send out messages.
*/
public function execute() {
$starttime = time();
$this->recipient = \core_user::get_user($this->get_userid());
$this->log_start("Sending forum digests for {$this->recipient->username} ({$this->recipient->id})");
if (empty($this->recipient->mailformat) || $this->recipient->mailformat != 1) {
// This user does not want to receive HTML.
$this->allowhtml = false;
}
// Fetch all of the data we need to mail these posts.
$this->prepare_data($starttime);
if (empty($this->posts) || empty($this->discussions) || empty($this->forums)) {
$this->log_finish("No messages found to send.");
return;
}
// Add the message headers.
$this->add_message_header();
foreach ($this->discussions as $discussion) {
// Raise the time limit for each discussion.
\core_php_time_limit::raise(120);
// Grab the data pertaining to this discussion.
$forum = $this->forums[$discussion->forum];
$course = $this->courses[$forum->course];
$cm = get_fast_modinfo($course)->instances['forum'][$forum->id];
$modcontext = \context_module::instance($cm->id);
$coursecontext = \context_course::instance($course->id);
if (empty($this->posts[$discussion->id])) {
// Somehow there are no posts.
// This should not happen but better safe than sorry.
continue;
}
if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
// The course is hidden and the user does not have access to it.
// Permissions may have changed since it was queued.
continue;
}
if (!forum_user_can_see_discussion($forum, $discussion, $modcontext, $this->recipient)) {
// User cannot see this discussion.
// Permissions may have changed since it was queued.
continue;
}
if (!\mod_forum\subscriptions::is_subscribed($this->recipient->id, $forum, $discussion->id, $cm)) {
// The user does not subscribe to this forum as a whole, or to this specific discussion.
continue;
}
// Fetch additional values relating to this forum.
if (!isset($this->canpostto[$discussion->id])) {
$this->canpostto[$discussion->id] = forum_user_can_post(
$forum, $discussion, $this->recipient, $cm, $course, $modcontext);
}
if (!isset($this->viewfullnames[$forum->id])) {
$this->viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $this->recipient->id);
}
// Set the discussion storage values.
$discussionpostcount = 0;
$this->discussiontext = '';
$this->discussionhtml = '';
// Add the header for this discussion.
$this->add_discussion_header($discussion, $forum, $course);
$this->log_start("Adding messages in discussion {$discussion->id} (forum {$forum->id})", 1);
// Add all posts in this forum.
foreach ($this->posts[$discussion->id] as $post) {
$author = $this->get_post_author($post->userid, $course, $forum, $cm, $modcontext);
if (empty($author)) {
// Unable to find the author. Skip to avoid errors.
continue;
}
if (!forum_user_can_see_post($forum, $discussion, $post, $this->recipient, $cm)) {
// User cannot see this post.
// Permissions may have changed since it was queued.
continue;
}
$this->add_post_body($author, $post, $discussion, $forum, $cm, $course);
$discussionpostcount++;
}
// Add the forum footer.
$this->add_discussion_footer($discussion, $forum, $course);
// Add the data for this discussion to the notification body.
if ($discussionpostcount) {
$this->sentcount += $discussionpostcount;
$this->notificationtext .= $this->discussiontext;
$this->notificationhtml .= $this->discussionhtml;
$this->log_finish("Added {$discussionpostcount} messages to discussion {$discussion->id}", 1);
} else {
$this->log_finish("No messages found in discussion {$discussion->id} - skipped.", 1);
}
}
if ($this->sentcount) {
// This digest has at least one post and should therefore be sent.
if ($this->send_mail()) {
$this->log_finish("Digest sent with {$this->sentcount} messages.");
if (get_user_preferences('forum_markasreadonnotification', 1, $this->recipient->id) == 1) {
forum_tp_mark_posts_read($this->recipient, $this->markpostsasread);
}
} else {
$this->log_finish("Issue sending digest. Skipping.");
}
} else {
$this->log_finish("No messages found to send.");
}
// We have finishied all digest emails, update $CFG->digestmailtimelast.
set_config('digestmailtimelast', $starttime);
}
/**
* Prepare the data for this run.
*
* Note: This will also remove posts from the queue.
*
* @param int $timenow
*/
protected function prepare_data(int $timenow) {
global $DB;
$sql = "SELECT p.*, f.id AS forum, f.course
FROM {forum_queue} q
INNER JOIN {forum_posts} p ON p.id = q.postid
INNER JOIN {forum_discussions} d ON d.id = p.discussion
INNER JOIN {forum} f ON f.id = d.forum
WHERE q.userid = :userid
AND q.timemodified < :timemodified
ORDER BY d.id, q.timemodified ASC";
$queueparams = [
'userid' => $this->recipient->id,
'timemodified' => $timenow,
];
$posts = $DB->get_recordset_sql($sql, $queueparams);
$discussionids = [];
$forumids = [];
$courseids = [];
$userids = [];
foreach ($posts as $post) {
$discussionids[] = $post->discussion;
$forumids[] = $post->forum;
$courseids[] = $post->course;
$userids[] = $post->userid;
unset($post->forum);
if (!isset($this->posts[$post->discussion])) {
$this->posts[$post->discussion] = [];
}
$this->posts[$post->discussion][$post->id] = $post;
}
$posts->close();
if (empty($discussionids)) {
// All posts have been removed since the task was queued.
$this->empty_queue($this->recipient->id, $timenow);
return;
}
list($in, $params) = $DB->get_in_or_equal($discussionids);
$this->discussions = $DB->get_records_select('forum_discussions', "id {$in}", $params);
list($in, $params) = $DB->get_in_or_equal($forumids);
$this->forums = $DB->get_records_select('forum', "id {$in}", $params);
list($in, $params) = $DB->get_in_or_equal($courseids);
$this->courses = $DB->get_records_select('course', "id $in", $params);
list($in, $params) = $DB->get_in_or_equal($userids);
$this->users = $DB->get_records_select('user', "id $in", $params);
$this->fill_digest_cache();
$this->empty_queue($this->recipient->id, $timenow);
}
/**
* Empty the queue of posts for this user.
*
* @param array $queueparams The list of
*/
protected function empty_queue(int $userid, int $timemodified) : void {
global $DB;
$DB->delete_records_select('forum_queue', "userid = :userid AND timemodified < :timemodified", [
'userid' => $userid,
'timemodified' => $timemodified,
]);
}
/**
* Fill the cron digest cache.
*/
protected function fill_digest_cache() {
global $DB;
$this->forumdigesttypes = $DB->get_records_menu('forum_digests', [
'userid' => $this->recipient->id,
], '', 'forum, maildigest');
}
/**
* Fetch and initialise the post author.
*
* @param int $userid The id of the user to fetch
* @param \stdClass $course
* @param \stdClass $forum
* @param \stdClass $cm
* @param \context $context
* @return \stdClass
*/
protected function get_post_author($userid, $course, $forum, $cm, $context) {
if (!isset($this->users[$userid])) {
// This user no longer exists.
return false;
}
$user = $this->users[$userid];
if (!isset($user->groups)) {
// Initialise the groups list.
$user->groups = [];
}
if (!isset($user->groups[$forum->id])) {
$user->groups[$forum->id] = groups_get_all_groups($course->id, $user->id, $cm->groupingid);
}
// Clone the user object to prevent leaks between messages.
return (object) (array) $user;
}
/**
* Add the header to this message.
*/
protected function add_message_header() {
$site = get_site();
// Set the subject of the message.
$this->postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true));
// And the content of the header in body.
$headerdata = (object) [
'sitename' => format_string($site->fullname, true),
'userprefs' => (new \moodle_url('/user/forum.php', [
'id' => $this->recipient->id,
'course' => $site->id,
]))->out(false),
];
$this->notificationtext .= get_string('digestmailheader', 'forum', $headerdata) . "\n";
if ($this->allowhtml) {
$headerdata->userprefs = html_writer::link($headerdata->userprefs, get_string('digestmailprefs', 'forum'), [
'target' => '_blank',
]);
$this->notificationhtml .= html_writer::tag('p', get_string('digestmailheader', 'forum', $headerdata));
$this->notificationhtml .= html_writer::empty_tag('br');
$this->notificationhtml .= html_writer::empty_tag('hr', [
'size' => 1,
'noshade' => 'noshade',
]);
}
}
/**
* Add the header for this discussion.
*
* @param \stdClass $discussion The discussion to add the footer for
* @param \stdClass $forum The forum that the discussion belongs to
* @param \stdClass $course The course that the forum belongs to
*/
protected function add_discussion_header($discussion, $forum, $course) {
global $CFG;
$shortname = format_string($course->shortname, true, [
'context' => \context_course::instance($course->id),
]);
$strforums = get_string('forums', 'forum');
$this->discussiontext .= "\n=====================================================================\n\n";
$this->discussiontext .= "$shortname -> $strforums -> " . format_string($forum->name, true);
if ($discussion->name != $forum->name) {
$this->discussiontext .= " -> " . format_string($discussion->name, true);
}
$this->discussiontext .= "\n";
$this->discussiontext .= new \moodle_url('/mod/forum/discuss.php', [
'd' => $discussion->id,
]);
$this->discussiontext .= "\n";
if ($this->allowhtml) {
$this->discussionhtml .= "<p><font face=\"sans-serif\">".
"<a target=\"_blank\" href=\"$CFG->wwwroot/course/view.php?id=$course->id\">$shortname</a> -> ".
"<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/index.php?id=$course->id\">$strforums</a> -> ".
"<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/view.php?f=$forum->id\">" .
format_string($forum->name, true)."</a>";
if ($discussion->name == $forum->name) {
$this->discussionhtml .= "</font></p>";
} else {
$this->discussionhtml .=
" -> <a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id\">" .
format_string($discussion->name, true)."</a></font></p>";
}
$this->discussionhtml .= '<p>';
}
}
/**
* Add the body of this post.
*
* @param \stdClass $author The author of the post
* @param \stdClass $post The post being sent
* @param \stdClass $discussion The discussion that the post is in
* @param \stdClass $forum The forum that the discussion belongs to
* @param \cminfo $cm The cminfo object for the forum
* @param \stdClass $course The course that the forum belongs to
*/
protected function add_post_body($author, $post, $discussion, $forum, $cm, $course) {
global $CFG;
$canreply = $this->canpostto[$discussion->id];
$data = new \mod_forum\output\forum_post_email(
$course,
$cm,
$forum,
$discussion,
$post,
$author,
$this->recipient,
$canreply
);
// Override the viewfullnames value.
$data->viewfullnames = $this->viewfullnames[$forum->id];
// Determine the type of digest being sent.
$maildigest = $this->get_maildigest($forum->id);
$textrenderer = $this->get_renderer($maildigest);
$this->discussiontext .= $textrenderer->render($data);
$this->discussiontext .= "\n";
if ($this->allowhtml) {
$htmlrenderer = $this->get_renderer($maildigest, true);
$this->discussionhtml .= $htmlrenderer->render($data);
$this->log("Adding post {$post->id} in format {$maildigest} with HTML", 2);
} else {
$this->log("Adding post {$post->id} in format {$maildigest} without HTML", 2);
}
if ($maildigest == 1 && $CFG->forum_usermarksread) {
// Create an array of postid's for this user to mark as read.
$this->markpostsasread[] = $post->id;
}
}
/**
* Add the footer for this discussion.
*
* @param \stdClass $discussion The discussion to add the footer for
*/
protected function add_discussion_footer($discussion) {
global $CFG;
if ($this->allowhtml) {
$footerlinks = [];
$forum = $this->forums[$discussion->forum];
if (\mod_forum\subscriptions::is_forcesubscribed($forum)) {
// This forum is force subscribed. The user cannot unsubscribe.
$footerlinks[] = get_string("everyoneissubscribed", "forum");
} else {
$footerlinks[] = "<a href=\"$CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\">" .
get_string("unsubscribe", "forum") . "</a>";
}
$footerlinks[] = "<a href='{$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}'>" .
get_string("digestmailpost", "forum") . '</a>';
$this->discussionhtml .= "\n<div class='mdl-right'><font size=\"1\">" .
implode('&nbsp;', $footerlinks) . '</font></div>';
$this->discussionhtml .= '<hr size="1" noshade="noshade" /></p>';
}
}
/**
* Get the forum digest type for the specified forum, failing back to
* the default setting for the current user if not specified.
*
* @param int $forumid
* @return int
*/
protected function get_maildigest($forumid) {
$maildigest = -1;
if (isset($this->forumdigesttypes[$forumid])) {
$maildigest = $this->forumdigesttypes[$forumid];
}
if ($maildigest === -1 && !empty($this->recipient->maildigest)) {
$maildigest = $this->recipient->maildigest;
}
if ($maildigest === -1) {
// There is no maildigest type right now.
$maildigest = 1;
}
return $maildigest;
}
/**
* Send the composed message to the user.
*/
protected function send_mail() {
// Headers to help prevent auto-responders.
$userfrom = \core_user::get_noreply_user();
$userfrom->customheaders = array(
"Precedence: Bulk",
'X-Auto-Response-Suppress: All',
'Auto-Submitted: auto-generated',
);
$eventdata = new \core\message\message();
$eventdata->courseid = SITEID;
$eventdata->component = 'mod_forum';
$eventdata->name = 'digests';
$eventdata->userfrom = $userfrom;
$eventdata->userto = $this->recipient;
$eventdata->subject = $this->postsubject;
$eventdata->fullmessage = $this->notificationtext;
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = $this->notificationhtml;
$eventdata->notification = 1;
$eventdata->smallmessage = get_string('smallmessagedigest', 'forum', $this->sentcount);
return message_send($eventdata);
}
/**
* Helper to fetch the required renderer, instantiating as required.
*
* @param int $maildigest The type of mail digest being sent
* @param bool $html Whether to fetch the HTML renderer
* @return \core_renderer
*/
protected function get_renderer($maildigest, $html = false) {
global $PAGE;
$type = $maildigest == 2 ? 'emaildigestbasic' : 'emaildigestfull';
$target = $html ? 'htmlemail' : 'textemail';
if (!isset($this->renderers[$target][$type])) {
$this->renderers[$target][$type] = $PAGE->get_renderer('mod_forum', $type, $target);
}
return $this->renderers[$target][$type];
}
}
@@ -0,0 +1,542 @@
<?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/>.
/**
* This file defines an adhoc task to send notifications.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_forum\task;
defined('MOODLE_INTERNAL') || die();
/**
* Adhoc task to send user forum notifications.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class send_user_notifications extends \core\task\adhoc_task {
// Use the logging trait to get some nice, juicy, logging.
use \core\task\logging_trait;
/**
* @var \stdClass A shortcut to $USER.
*/
protected $recipient;
/**
* @var \stdClass[] List of courses the messages are in, indexed by courseid.
*/
protected $courses = [];
/**
* @var \stdClass[] List of forums the messages are in, indexed by courseid.
*/
protected $forums = [];
/**
* @var int[] List of IDs for forums in each course.
*/
protected $courseforums = [];
/**
* @var \stdClass[] List of discussions the messages are in, indexed by forumid.
*/
protected $discussions = [];
/**
* @var \stdClass[] List of IDs for discussions in each forum.
*/
protected $forumdiscussions = [];
/**
* @var \stdClass[] List of posts the messages are in, indexed by discussionid.
*/
protected $posts = [];
/**
* @var bool[] Whether the user can view fullnames for each forum.
*/
protected $viewfullnames = [];
/**
* @var bool[] Whether the user can post in each discussion.
*/
protected $canpostto = [];
/**
* @var \renderer[] The renderers.
*/
protected $renderers = [];
/**
* @var \core\message\inbound\address_manager The inbound message address manager.
*/
protected $inboundmanager;
/**
* Send out messages.
*/
public function execute() {
global $CFG;
// Raise the time limit for each discussion.
\core_php_time_limit::raise(120);
$this->recipient = \core_user::get_user($this->get_userid());
// Create the generic messageinboundgenerator.
$this->inboundmanager = new \core\message\inbound\address_manager();
$this->inboundmanager->set_handler('\mod_forum\message\inbound\reply_handler');
$data = $this->get_custom_data();
$this->prepare_data((array) $data);
$markposts = [];
$errorcount = 0;
$sentcount = 0;
$this->log_start("Sending messages to {$this->recipient->username} ({$this->recipient->id})");
foreach ($this->courses as $course) {
$coursecontext = \context_course::instance($course->id);
if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
// The course is hidden and the user does not have access to it.
// Permissions may have changed since it was queued.
continue;
}
foreach ($this->courseforums[$course->id] as $forumid) {
$forum = $this->forums[$forumid];
$cm = get_fast_modinfo($course)->instances['forum'][$forumid];
$modcontext = \context_module::instance($cm->id);
foreach (array_values($this->forumdiscussions[$forumid]) as $discussionid) {
$discussion = $this->discussions[$discussionid];
if (!forum_user_can_see_discussion($forum, $discussion, $modcontext, $this->recipient)) {
// User cannot see this discussion.
// Permissions may have changed since it was queued.
continue;
}
if (!\mod_forum\subscriptions::is_subscribed($this->recipient->id, $forum, $discussionid, $cm)) {
// The user does not subscribe to this forum as a whole, or to this specific discussion.
continue;
}
foreach ($this->posts[$discussionid] as $post) {
if (!forum_user_can_see_post($forum, $discussion, $post, $this->recipient, $cm)) {
// User cannot see this post.
// Permissions may have changed since it was queued.
continue;
}
if ($this->send_post($course, $forum, $discussion, $post, $cm, $modcontext)) {
$this->log("Post {$post->id} sent", 1);
// Mark post as read if forum_usermarksread is set off.
if (!$CFG->forum_usermarksread) {
$markposts[$post->id] = true;
}
$sentcount++;
} else {
$this->log("Failed to send post {$post->id}", 1);
$errorcount++;
}
}
}
}
}
$this->log_finish("Sent {$sentcount} messages with {$errorcount} failures");
if (!empty($markposts)) {
if (get_user_preferences('forum_markasreadonnotification', 1, $this->recipient->id) == 1) {
$this->log_start("Marking posts as read");
$count = count($markposts);
forum_tp_mark_posts_read($this->recipient, array_keys($markposts));
$this->log_finish("Marked {$count} posts as read");
}
}
}
/**
* Prepare all data for this run.
*
* Take all post ids, and fetch the relevant authors, discussions, forums, and courses for them.
*
* @param int[] $postids The list of post IDs
*/
protected function prepare_data(array $postids) {
global $DB;
if (empty($postids)) {
return;
}
list($in, $params) = $DB->get_in_or_equal(array_values($postids));
$sql = "SELECT p.*, f.id AS forum, f.course
FROM {forum_posts} p
INNER JOIN {forum_discussions} d ON d.id = p.discussion
INNER JOIN {forum} f ON f.id = d.forum
WHERE p.id {$in}";
$posts = $DB->get_recordset_sql($sql, $params);
$discussionids = [];
$forumids = [];
$courseids = [];
$userids = [];
foreach ($posts as $post) {
$discussionids[] = $post->discussion;
$forumids[] = $post->forum;
$courseids[] = $post->course;
$userids[] = $post->userid;
unset($post->forum);
if (!isset($this->posts[$post->discussion])) {
$this->posts[$post->discussion] = [];
}
$this->posts[$post->discussion][$post->id] = $post;
}
$posts->close();
if (empty($discussionids)) {
// All posts have been removed since the task was queued.
return;
}
// Fetch all discussions.
list($in, $params) = $DB->get_in_or_equal(array_values($discussionids));
$this->discussions = $DB->get_records_select('forum_discussions', "id {$in}", $params);
foreach ($this->discussions as $discussion) {
if (empty($this->forumdiscussions[$discussion->forum])) {
$this->forumdiscussions[$discussion->forum] = [];
}
$this->forumdiscussions[$discussion->forum][] = $discussion->id;
}
// Fetch all forums.
list($in, $params) = $DB->get_in_or_equal(array_values($forumids));
$this->forums = $DB->get_records_select('forum', "id {$in}", $params);
foreach ($this->forums as $forum) {
if (empty($this->courseforums[$forum->course])) {
$this->courseforums[$forum->course] = [];
}
$this->courseforums[$forum->course][] = $forum->id;
}
// Fetch all courses.
list($in, $params) = $DB->get_in_or_equal(array_values($courseids));
$this->courses = $DB->get_records_select('course', "id $in", $params);
// Fetch all authors.
list($in, $params) = $DB->get_in_or_equal(array_values($userids));
$users = $DB->get_recordset_select('user', "id $in", $params);
foreach ($users as $user) {
$this->minimise_user_record($user);
$this->users[$user->id] = $user;
}
$users->close();
// Fill subscription caches for each forum.
// These are per-user.
foreach (array_values($forumids) as $id) {
\mod_forum\subscriptions::fill_subscription_cache($id);
\mod_forum\subscriptions::fill_discussion_subscription_cache($id);
}
}
/**
* Send the specified post for the current user.
*
* @param \stdClass $course
* @param \stdClass $forum
* @param \stdClass $discussion
* @param \stdClass $post
* @param \stdClass $cm
* @param \context $context
*/
protected function send_post($course, $forum, $discussion, $post, $cm, $context) {
global $CFG;
$author = $this->get_post_author($post->userid, $course, $forum, $cm, $context);
if (empty($author)) {
return false;
}
// Prepare to actually send the post now, and build up the content.
$cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name)));
$shortname = format_string($course->shortname, true, [
'context' => \context_course::instance($course->id),
]);
// Generate a reply-to address from using the Inbound Message handler.
$replyaddress = $this->get_reply_address($course, $forum, $discussion, $post, $cm, $context);
$data = new \mod_forum\output\forum_post_email(
$course,
$cm,
$forum,
$discussion,
$post,
$author,
$this->recipient,
$this->can_post($course, $forum, $discussion, $post, $cm, $context)
);
$data->viewfullnames = $this->can_view_fullnames($course, $forum, $discussion, $post, $cm, $context);
// Not all of these variables are used in the default string but are made available to support custom subjects.
$site = get_site();
$a = (object) [
'subject' => $data->get_subject(),
'forumname' => $cleanforumname,
'sitefullname' => format_string($site->fullname),
'siteshortname' => format_string($site->shortname),
'courseidnumber' => $data->get_courseidnumber(),
'coursefullname' => $data->get_coursefullname(),
'courseshortname' => $data->get_coursename(),
];
$postsubject = html_to_text(get_string('postmailsubject', 'forum', $a), 0);
// Message headers are stored against the message author.
$author->customheaders = $this->get_message_headers($course, $forum, $discussion, $post, $a, $data);
$eventdata = new \core\message\message();
$eventdata->courseid = $course->id;
$eventdata->component = 'mod_forum';
$eventdata->name = 'posts';
$eventdata->userfrom = $author;
$eventdata->userto = $this->recipient;
$eventdata->subject = $postsubject;
$eventdata->fullmessage = $this->get_renderer()->render($data);
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = $this->get_renderer(true)->render($data);
$eventdata->notification = 1;
$eventdata->replyto = $replyaddress;
if (!empty($replyaddress)) {
// Add extra text to email messages if they can reply back.
$eventdata->set_additional_content('email', [
'fullmessage' => [
'footer' => "\n\n" . get_string('replytopostbyemail', 'mod_forum'),
],
'fullmessagehtml' => [
'footer' => \html_writer::tag('p', get_string('replytopostbyemail', 'mod_forum')),
]
]);
}
$eventdata->smallmessage = get_string('smallmessage', 'forum', (object) [
'user' => fullname($author),
'forumname' => "$shortname: " . format_string($forum->name, true) . ": " . $discussion->name,
'message' => $post->message,
]);
$contexturl = new \moodle_url('/mod/forum/discuss.php', ['d' => $discussion->id], "p{$post->id}");
$eventdata->contexturl = $contexturl->out();
$eventdata->contexturlname = $discussion->name;
return message_send($eventdata);
}
/**
* Fetch and initialise the post author.
*
* @param int $userid The id of the user to fetch
* @param \stdClass $course
* @param \stdClass $forum
* @param \stdClass $cm
* @param \context $context
* @return \stdClass
*/
protected function get_post_author($userid, $course, $forum, $cm, $context) {
if (!isset($this->users[$userid])) {
// This user no longer exists.
return false;
}
$user = $this->users[$userid];
if (!isset($user->groups)) {
// Initialise the groups list.
$user->groups = [];
}
if (!isset($user->groups[$forum->id])) {
$user->groups[$forum->id] = groups_get_all_groups($course->id, $user->id, $cm->groupingid);
}
// Clone the user object to prevent leaks between messages.
return (object) (array) $user;
}
/**
* Helper to fetch the required renderer, instantiating as required.
*
* @param bool $html Whether to fetch the HTML renderer
* @return \core_renderer
*/
protected function get_renderer($html = false) {
global $PAGE;
$target = $html ? 'htmlemail' : 'textemail';
if (!isset($this->renderers[$target])) {
$this->renderers[$target] = $PAGE->get_renderer('mod_forum', 'email', $target);
}
return $this->renderers[$target];
}
/**
* Get the list of message headers.
*
* @param \stdClass $course
* @param \stdClass $forum
* @param \stdClass $discussion
* @param \stdClass $post
* @param \stdClass $a The list of strings for this post
* @param \core\message\message $message The message to be sent
* @return \stdClass
*/
protected function get_message_headers($course, $forum, $discussion, $post, $a, $message) {
$cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name)));
$viewurl = new \moodle_url('/mod/forum/view.php', ['f' => $forum->id]);
$headers = [
// Headers to make emails easier to track.
'List-Id: "' . $cleanforumname . '" ' . generate_email_messageid('moodleforum' . $forum->id),
'List-Help: ' . $viewurl->out(),
'Message-ID: ' . forum_get_email_message_id($post->id, $this->recipient->id),
'X-Course-Id: ' . $course->id,
'X-Course-Name: '. format_string($course->fullname, true),
// Headers to help prevent auto-responders.
'Precedence: Bulk',
'X-Auto-Response-Suppress: All',
'Auto-Submitted: auto-generated',
'List-Unsubscribe: <' . $message->get_unsubscribediscussionlink() . '>',
];
$rootid = forum_get_email_message_id($discussion->firstpost, $this->recipient->id);
if ($post->parent) {
// This post is a reply, so add reply header (RFC 2822).
$parentid = forum_get_email_message_id($post->parent, $this->recipient->id);
$headers[] = "In-Reply-To: $parentid";
// If the post is deeply nested we also reference the parent message id and
// the root message id (if different) to aid threading when parts of the email
// conversation have been deleted (RFC1036).
if ($post->parent != $discussion->firstpost) {
$headers[] = "References: $rootid $parentid";
} else {
$headers[] = "References: $parentid";
}
}
// MS Outlook / Office uses poorly documented and non standard headers, including
// Thread-Topic which overrides the Subject and shouldn't contain Re: or Fwd: etc.
$aclone = (object) (array) $a;
$aclone->subject = $discussion->name;
$threadtopic = html_to_text(get_string('postmailsubject', 'forum', $aclone), 0);
$headers[] = "Thread-Topic: $threadtopic";
$headers[] = "Thread-Index: " . substr($rootid, 1, 28);
return $headers;
}
/**
* Get a no-reply address for this user to reply to the current post.
*
* @param \stdClass $course
* @param \stdClass $forum
* @param \stdClass $discussion
* @param \stdClass $post
* @param \stdClass $cm
* @param \context $context
* @return string
*/
protected function get_reply_address($course, $forum, $discussion, $post, $cm, $context) {
if ($this->can_post($course, $forum, $discussion, $post, $cm, $context)) {
// Generate a reply-to address from using the Inbound Message handler.
$this->inboundmanager->set_data($post->id);
return $this->inboundmanager->generate($this->recipient->id);
}
// TODO Check if we can return a string.
// This will be controlled by the event.
return null;
}
/**
* Check whether the user can post.
*
* @param \stdClass $course
* @param \stdClass $forum
* @param \stdClass $discussion
* @param \stdClass $post
* @param \stdClass $cm
* @param \context $context
* @return bool
*/
protected function can_post($course, $forum, $discussion, $post, $cm, $context) {
if (!isset($this->canpostto[$discussion->id])) {
$this->canpostto[$discussion->id] = forum_user_can_post($forum, $discussion, $this->recipient, $cm, $course, $context);
}
return $this->canpostto[$discussion->id];
}
/**
* Check whether the user can view full names of other users.
*
* @param \stdClass $course
* @param \stdClass $forum
* @param \stdClass $discussion
* @param \stdClass $post
* @param \stdClass $cm
* @param \context $context
* @return bool
*/
protected function can_view_fullnames($course, $forum, $discussion, $post, $cm, $context) {
if (!isset($this->viewfullnames[$forum->id])) {
$this->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $context, $this->recipient->id);
}
return $this->viewfullnames[$forum->id];
}
/**
* Removes properties from user record that are not necessary for sending post notifications.
*
* @param \stdClass $user
*/
protected function minimise_user_record(\stdClass $user) {
// We store large amount of users in one huge array, make sure we do not store info there we do not actually
// need in mail generation code or messaging.
unset($user->institution);
unset($user->department);
unset($user->address);
unset($user->city);
unset($user->url);
unset($user->currentlogin);
unset($user->description);
unset($user->descriptionformat);
}
}
+38
View File
@@ -372,3 +372,41 @@ function forum_make_mail_post($course, $cm, $forum, $discussion, $post, $userfro
return $renderer->render($renderable);
}
/**
* Removes properties from user record that are not necessary for sending post notifications.
*
* @param stdClass $user
* @return void, $user parameter is modified
* @deprecated since Moodle 3.7
*/
function forum_cron_minimise_user_record(stdClass $user) {
debugging("forum_cron_minimise_user_record() has been deprecated and has not been replaced.",
DEBUG_DEVELOPER);
// We store large amount of users in one huge array,
// make sure we do not store info there we do not actually need
// in mail generation code or messaging.
unset($user->institution);
unset($user->department);
unset($user->address);
unset($user->city);
unset($user->url);
unset($user->currentlogin);
unset($user->description);
unset($user->descriptionformat);
}
/**
* Function to be run periodically according to the scheduled task.
*
* Finds all posts that have yet to be mailed out, and mails them out to all subscribers as well as other maintance
* tasks.
*
* @deprecated since Moodle 3.7
*/
function forum_cron() {
debugging("forum_cron() has been deprecated and replaced with new tasks. Please uses these instead.",
DEBUG_DEVELOPER);
}
-903
View File
@@ -423,829 +423,6 @@ function forum_get_email_message_id($postid, $usertoid) {
return generate_email_messageid(hash('sha256', $postid . 'to' . $usertoid));
}
/**
* Removes properties from user record that are not necessary
* for sending post notifications.
* @param stdClass $user
* @return void, $user parameter is modified
*/
function forum_cron_minimise_user_record(stdClass $user) {
// We store large amount of users in one huge array,
// make sure we do not store info there we do not actually need
// in mail generation code or messaging.
unset($user->institution);
unset($user->department);
unset($user->address);
unset($user->city);
unset($user->url);
unset($user->currentlogin);
unset($user->description);
unset($user->descriptionformat);
}
/**
* Function to be run periodically according to the scheduled task.
*
* Finds all posts that have yet to be mailed out, and mails them
* out to all subscribers as well as other maintance tasks.
*
* NOTE: Since 2.7.2 this function is run by scheduled task rather
* than standard cron.
*
* @todo MDL-44734 The function will be split up into seperate tasks.
*/
function forum_cron() {
global $CFG, $USER, $DB, $PAGE;
$site = get_site();
// The main renderers.
$htmlout = $PAGE->get_renderer('mod_forum', 'email', 'htmlemail');
$textout = $PAGE->get_renderer('mod_forum', 'email', 'textemail');
$htmldigestfullout = $PAGE->get_renderer('mod_forum', 'emaildigestfull', 'htmlemail');
$textdigestfullout = $PAGE->get_renderer('mod_forum', 'emaildigestfull', 'textemail');
$htmldigestbasicout = $PAGE->get_renderer('mod_forum', 'emaildigestbasic', 'htmlemail');
$textdigestbasicout = $PAGE->get_renderer('mod_forum', 'emaildigestbasic', 'textemail');
// All users that are subscribed to any post that needs sending,
// please increase $CFG->extramemorylimit on large sites that
// send notifications to a large number of users.
$users = array();
$userscount = 0; // Cached user counter - count($users) in PHP is horribly slow!!!
// Status arrays.
$mailcount = array();
$errorcount = array();
// caches
$discussions = array();
$forums = array();
$courses = array();
$coursemodules = array();
$subscribedusers = array();
$messageinboundhandlers = array();
// Posts older than 2 days will not be mailed. This is to avoid the problem where
// cron has not been running for a long time, and then suddenly people are flooded
// with mail from the past few weeks or months
$timenow = time();
$endtime = $timenow - $CFG->maxeditingtime;
$starttime = $endtime - 48 * 3600; // Two days earlier
// Get the list of forum subscriptions for per-user per-forum maildigest settings.
$digestsset = $DB->get_recordset('forum_digests', null, '', 'id, userid, forum, maildigest');
$digests = array();
foreach ($digestsset as $thisrow) {
if (!isset($digests[$thisrow->forum])) {
$digests[$thisrow->forum] = array();
}
$digests[$thisrow->forum][$thisrow->userid] = $thisrow->maildigest;
}
$digestsset->close();
// Create the generic messageinboundgenerator.
$messageinboundgenerator = new \core\message\inbound\address_manager();
$messageinboundgenerator->set_handler('\mod_forum\message\inbound\reply_handler');
if ($posts = forum_get_unmailed_posts($starttime, $endtime, $timenow)) {
// Mark them all now as being mailed. It's unlikely but possible there
// might be an error later so that a post is NOT actually mailed out,
// but since mail isn't crucial, we can accept this risk. Doing it now
// prevents the risk of duplicated mails, which is a worse problem.
if (!forum_mark_old_posts_as_mailed($endtime)) {
mtrace('Errors occurred while trying to mark some posts as being mailed.');
return false; // Don't continue trying to mail them, in case we are in a cron loop
}
// checking post validity, and adding users to loop through later
foreach ($posts as $pid => $post) {
$discussionid = $post->discussion;
if (!isset($discussions[$discussionid])) {
if ($discussion = $DB->get_record('forum_discussions', array('id'=> $post->discussion))) {
$discussions[$discussionid] = $discussion;
\mod_forum\subscriptions::fill_subscription_cache($discussion->forum);
\mod_forum\subscriptions::fill_discussion_subscription_cache($discussion->forum);
} else {
mtrace('Could not find discussion ' . $discussionid);
unset($posts[$pid]);
continue;
}
}
$forumid = $discussions[$discussionid]->forum;
if (!isset($forums[$forumid])) {
if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
$forums[$forumid] = $forum;
} else {
mtrace('Could not find forum '.$forumid);
unset($posts[$pid]);
continue;
}
}
$courseid = $forums[$forumid]->course;
if (!isset($courses[$courseid])) {
if ($course = $DB->get_record('course', array('id' => $courseid))) {
$courses[$courseid] = $course;
} else {
mtrace('Could not find course '.$courseid);
unset($posts[$pid]);
continue;
}
}
if (!isset($coursemodules[$forumid])) {
if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
$coursemodules[$forumid] = $cm;
} else {
mtrace('Could not find course module for forum '.$forumid);
unset($posts[$pid]);
continue;
}
}
$modcontext = context_module::instance($coursemodules[$forumid]->id);
// Save the Inbound Message datakey here to reduce DB queries later.
$messageinboundgenerator->set_data($pid);
$messageinboundhandlers[$pid] = $messageinboundgenerator->fetch_data_key();
// Caching subscribed users of each forum.
if (!isset($subscribedusers[$forumid])) {
if ($subusers = \mod_forum\subscriptions::fetch_subscribed_users($forums[$forumid], 0, $modcontext, 'u.*', true)) {
foreach ($subusers as $postuser) {
// this user is subscribed to this forum
$subscribedusers[$forumid][$postuser->id] = $postuser->id;
$userscount++;
if ($userscount > FORUM_CRON_USER_CACHE) {
// Store minimal user info.
$minuser = new stdClass();
$minuser->id = $postuser->id;
$users[$postuser->id] = $minuser;
} else {
// Cache full user record.
forum_cron_minimise_user_record($postuser);
$users[$postuser->id] = $postuser;
}
}
// Release memory.
unset($subusers);
unset($postuser);
}
}
$mailcount[$pid] = 0;
$errorcount[$pid] = 0;
}
}
if ($users && $posts) {
foreach ($users as $userto) {
// Terminate if processing of any account takes longer than 2 minutes.
core_php_time_limit::raise(120);
mtrace('Processing user ' . $userto->id);
// Init user caches - we keep the cache for one cycle only, otherwise it could consume too much memory.
if (isset($userto->username)) {
$userto = clone($userto);
} else {
$userto = $DB->get_record('user', array('id' => $userto->id));
forum_cron_minimise_user_record($userto);
}
$userto->viewfullnames = array();
$userto->canpost = array();
$userto->markposts = array();
// Setup this user so that the capabilities are cached, and environment matches receiving user.
cron_setup_user($userto);
// Reset the caches.
foreach ($coursemodules as $forumid => $unused) {
$coursemodules[$forumid]->cache = new stdClass();
$coursemodules[$forumid]->cache->caps = array();
unset($coursemodules[$forumid]->uservisible);
}
foreach ($posts as $pid => $post) {
$discussion = $discussions[$post->discussion];
$forum = $forums[$discussion->forum];
$course = $courses[$forum->course];
$cm =& $coursemodules[$forum->id];
// Do some checks to see if we can bail out now.
// Only active enrolled users are in the list of subscribers.
// This does not necessarily mean that the user is subscribed to the forum or to the discussion though.
if (!isset($subscribedusers[$forum->id][$userto->id])) {
// The user does not subscribe to this forum.
continue;
}
if (!\mod_forum\subscriptions::is_subscribed($userto->id, $forum, $post->discussion, $coursemodules[$forum->id])) {
// The user does not subscribe to this forum, or to this specific discussion.
continue;
}
if ($subscriptiontime = \mod_forum\subscriptions::fetch_discussion_subscription($forum->id, $userto->id)) {
// Skip posts if the user subscribed to the discussion after it was created.
if (isset($subscriptiontime[$post->discussion]) && ($subscriptiontime[$post->discussion] > $post->created)) {
continue;
}
}
$coursecontext = context_course::instance($course->id);
if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext, $userto->id)) {
// The course is hidden and the user does not have access to it.
continue;
}
// Don't send email if the forum is Q&A and the user has not posted.
// Initial topics are still mailed.
if ($forum->type == 'qanda' && !forum_get_user_posted_time($discussion->id, $userto->id) && $pid != $discussion->firstpost) {
mtrace('Did not email ' . $userto->id.' because user has not posted in discussion');
continue;
}
// Get info about the sending user.
if (array_key_exists($post->userid, $users)) {
// We might know the user already.
$userfrom = $users[$post->userid];
if (!isset($userfrom->idnumber)) {
// Minimalised user info, fetch full record.
$userfrom = $DB->get_record('user', array('id' => $userfrom->id));
forum_cron_minimise_user_record($userfrom);
}
} else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
forum_cron_minimise_user_record($userfrom);
// Fetch only once if possible, we can add it to user list, it will be skipped anyway.
if ($userscount <= FORUM_CRON_USER_CACHE) {
$userscount++;
$users[$userfrom->id] = $userfrom;
}
} else {
mtrace('Could not find user ' . $post->userid . ', author of post ' . $post->id . '. Unable to send message.');
continue;
}
// Note: If we want to check that userto and userfrom are not the same person this is probably the spot to do it.
// Setup global $COURSE properly - needed for roles and languages.
cron_setup_user($userto, $course);
// Fill caches.
if (!isset($userto->viewfullnames[$forum->id])) {
$modcontext = context_module::instance($cm->id);
$userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
}
if (!isset($userto->canpost[$discussion->id])) {
$modcontext = context_module::instance($cm->id);
$userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
}
if (!isset($userfrom->groups[$forum->id])) {
if (!isset($userfrom->groups)) {
$userfrom->groups = array();
if (isset($users[$userfrom->id])) {
$users[$userfrom->id]->groups = array();
}
}
$userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
if (isset($users[$userfrom->id])) {
$users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
}
}
// Make sure groups allow this user to see this email.
if ($discussion->groupid > 0 and $groupmode = groups_get_activity_groupmode($cm, $course)) {
// Groups are being used.
if (!groups_group_exists($discussion->groupid)) {
// Can't find group - be safe and don't this message.
continue;
}
if (!groups_is_member($discussion->groupid) and !has_capability('moodle/site:accessallgroups', $modcontext)) {
// Do not send posts from other groups when in SEPARATEGROUPS or VISIBLEGROUPS.
continue;
}
}
// Make sure we're allowed to see the post.
if (!forum_user_can_see_post($forum, $discussion, $post, null, $cm)) {
mtrace('User ' . $userto->id .' can not see ' . $post->id . '. Not sending message.');
continue;
}
// OK so we need to send the email.
// Does the user want this post in a digest? If so postpone it for now.
$maildigest = forum_get_user_maildigest_bulk($digests, $userto, $forum->id);
if ($maildigest > 0) {
// This user wants the mails to be in digest form.
$queue = new stdClass();
$queue->userid = $userto->id;
$queue->discussionid = $discussion->id;
$queue->postid = $post->id;
$queue->timemodified = $post->created;
$DB->insert_record('forum_queue', $queue);
continue;
}
// Prepare to actually send the post now, and build up the content.
$cleanforumname = str_replace('"', "'", strip_tags(format_string($forum->name)));
$userfrom->customheaders = array (
// Headers to make emails easier to track.
'List-Id: "' . $cleanforumname . '" ' . generate_email_messageid('moodleforum' . $forum->id),
'List-Help: ' . $CFG->wwwroot . '/mod/forum/view.php?f=' . $forum->id,
'Message-ID: ' . forum_get_email_message_id($post->id, $userto->id),
'X-Course-Id: ' . $course->id,
'X-Course-Name: ' . format_string($course->fullname, true),
// Headers to help prevent auto-responders.
'Precedence: Bulk',
'X-Auto-Response-Suppress: All',
'Auto-Submitted: auto-generated',
);
$shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
// Generate a reply-to address from using the Inbound Message handler.
$replyaddress = null;
if ($userto->canpost[$discussion->id] && array_key_exists($post->id, $messageinboundhandlers)) {
$messageinboundgenerator->set_data($post->id, $messageinboundhandlers[$post->id]);
$replyaddress = $messageinboundgenerator->generate($userto->id);
}
if (!isset($userto->canpost[$discussion->id])) {
$canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
} else {
$canreply = $userto->canpost[$discussion->id];
}
$data = new \mod_forum\output\forum_post_email(
$course,
$cm,
$forum,
$discussion,
$post,
$userfrom,
$userto,
$canreply
);
$userfrom->customheaders[] = sprintf('List-Unsubscribe: <%s>',
$data->get_unsubscribediscussionlink());
if (!isset($userto->viewfullnames[$forum->id])) {
$data->viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
} else {
$data->viewfullnames = $userto->viewfullnames[$forum->id];
}
// Not all of these variables are used in the default language
// string but are made available to support custom subjects.
$a = new stdClass();
$a->subject = $data->get_subject();
$a->forumname = $cleanforumname;
$a->sitefullname = format_string($site->fullname);
$a->siteshortname = format_string($site->shortname);
$a->courseidnumber = $data->get_courseidnumber();
$a->coursefullname = $data->get_coursefullname();
$a->courseshortname = $data->get_coursename();
$postsubject = html_to_text(get_string('postmailsubject', 'forum', $a), 0);
$rootid = forum_get_email_message_id($discussion->firstpost, $userto->id);
if ($post->parent) {
// This post is a reply, so add reply header (RFC 2822).
$parentid = forum_get_email_message_id($post->parent, $userto->id);
$userfrom->customheaders[] = "In-Reply-To: $parentid";
// If the post is deeply nested we also reference the parent message id and
// the root message id (if different) to aid threading when parts of the email
// conversation have been deleted (RFC1036).
if ($post->parent != $discussion->firstpost) {
$userfrom->customheaders[] = "References: $rootid $parentid";
} else {
$userfrom->customheaders[] = "References: $parentid";
}
}
// MS Outlook / Office uses poorly documented and non standard headers, including
// Thread-Topic which overrides the Subject and shouldn't contain Re: or Fwd: etc.
$a->subject = $discussion->name;
$threadtopic = html_to_text(get_string('postmailsubject', 'forum', $a), 0);
$userfrom->customheaders[] = "Thread-Topic: $threadtopic";
$userfrom->customheaders[] = "Thread-Index: " . substr($rootid, 1, 28);
// Send the post now!
mtrace('Sending ', '');
$eventdata = new \core\message\message();
$eventdata->courseid = $course->id;
$eventdata->component = 'mod_forum';
$eventdata->name = 'posts';
$eventdata->userfrom = $userfrom;
$eventdata->userto = $userto;
$eventdata->subject = $postsubject;
$eventdata->fullmessage = $textout->render($data);
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = $htmlout->render($data);
$eventdata->notification = 1;
$eventdata->replyto = $replyaddress;
if (!empty($replyaddress)) {
// Add extra text to email messages if they can reply back.
$textfooter = "\n\n" . get_string('replytopostbyemail', 'mod_forum');
$htmlfooter = html_writer::tag('p', get_string('replytopostbyemail', 'mod_forum'));
$additionalcontent = array('fullmessage' => array('footer' => $textfooter),
'fullmessagehtml' => array('footer' => $htmlfooter));
$eventdata->set_additional_content('email', $additionalcontent);
}
$smallmessagestrings = new stdClass();
$smallmessagestrings->user = fullname($userfrom);
$smallmessagestrings->forumname = "$shortname: " . format_string($forum->name, true) . ": " . $discussion->name;
$smallmessagestrings->message = $post->message;
// Make sure strings are in message recipients language.
$eventdata->smallmessage = get_string_manager()->get_string('smallmessage', 'forum', $smallmessagestrings, $userto->lang);
$contexturl = new moodle_url('/mod/forum/discuss.php', array('d' => $discussion->id), 'p' . $post->id);
$eventdata->contexturl = $contexturl->out();
$eventdata->contexturlname = $discussion->name;
$mailresult = message_send($eventdata);
if (!$mailresult) {
mtrace("Error: mod/forum/lib.php forum_cron(): Could not send out mail for id $post->id to user $userto->id".
" ($userto->email) .. not trying again.");
$errorcount[$post->id]++;
} else {
$mailcount[$post->id]++;
// Mark post as read if forum_usermarksread is set off.
if (!$CFG->forum_usermarksread) {
$userto->markposts[$post->id] = $post->id;
}
}
mtrace('post ' . $post->id . ': ' . $post->subject);
}
// Mark processed posts as read.
if (get_user_preferences('forum_markasreadonnotification', 1, $userto->id) == 1) {
forum_tp_mark_posts_read($userto, $userto->markposts);
}
unset($userto);
}
}
if ($posts) {
foreach ($posts as $post) {
mtrace($mailcount[$post->id]." users were sent post $post->id, '$post->subject'");
if ($errorcount[$post->id]) {
$DB->set_field('forum_posts', 'mailed', FORUM_MAILED_ERROR, array('id' => $post->id));
}
}
}
// release some memory
unset($subscribedusers);
unset($mailcount);
unset($errorcount);
cron_setup_user();
$sitetimezone = core_date::get_server_timezone();
// Now see if there are any digest mails waiting to be sent, and if we should send them
mtrace('Starting digest processing...');
core_php_time_limit::raise(300); // terminate if not able to fetch all digests in 5 minutes
if (!isset($CFG->digestmailtimelast)) { // To catch the first time
set_config('digestmailtimelast', 0);
}
$timenow = time();
$digesttime = usergetmidnight($timenow, $sitetimezone) + ($CFG->digestmailtime * 3600);
// Delete any really old ones (normally there shouldn't be any)
$weekago = $timenow - (7 * 24 * 3600);
$DB->delete_records_select('forum_queue', "timemodified < ?", array($weekago));
mtrace ('Cleaned old digest records');
if ($CFG->digestmailtimelast < $digesttime and $timenow > $digesttime) {
mtrace('Sending forum digests: '.userdate($timenow, '', $sitetimezone));
$digestposts_rs = $DB->get_recordset_select('forum_queue', "timemodified < ?", array($digesttime));
if ($digestposts_rs->valid()) {
// We have work to do
$usermailcount = 0;
//caches - reuse the those filled before too
$discussionposts = array();
$userdiscussions = array();
foreach ($digestposts_rs as $digestpost) {
if (!isset($posts[$digestpost->postid])) {
if ($post = $DB->get_record('forum_posts', array('id' => $digestpost->postid))) {
$posts[$digestpost->postid] = $post;
} else {
continue;
}
}
$discussionid = $digestpost->discussionid;
if (!isset($discussions[$discussionid])) {
if ($discussion = $DB->get_record('forum_discussions', array('id' => $discussionid))) {
$discussions[$discussionid] = $discussion;
} else {
continue;
}
}
$forumid = $discussions[$discussionid]->forum;
if (!isset($forums[$forumid])) {
if ($forum = $DB->get_record('forum', array('id' => $forumid))) {
$forums[$forumid] = $forum;
} else {
continue;
}
}
$courseid = $forums[$forumid]->course;
if (!isset($courses[$courseid])) {
if ($course = $DB->get_record('course', array('id' => $courseid))) {
$courses[$courseid] = $course;
} else {
continue;
}
}
if (!isset($coursemodules[$forumid])) {
if ($cm = get_coursemodule_from_instance('forum', $forumid, $courseid)) {
$coursemodules[$forumid] = $cm;
} else {
continue;
}
}
$userdiscussions[$digestpost->userid][$digestpost->discussionid] = $digestpost->discussionid;
$discussionposts[$digestpost->discussionid][$digestpost->postid] = $digestpost->postid;
}
$digestposts_rs->close(); /// Finished iteration, let's close the resultset
// Data collected, start sending out emails to each user
foreach ($userdiscussions as $userid => $thesediscussions) {
core_php_time_limit::raise(120); // terminate if processing of any account takes longer than 2 minutes
cron_setup_user();
mtrace(get_string('processingdigest', 'forum', $userid), '... ');
// First of all delete all the queue entries for this user
$DB->delete_records_select('forum_queue', "userid = ? AND timemodified < ?", array($userid, $digesttime));
// Init user caches - we keep the cache for one cycle only,
// otherwise it would unnecessarily consume memory.
if (array_key_exists($userid, $users) and isset($users[$userid]->username)) {
$userto = clone($users[$userid]);
} else {
$userto = $DB->get_record('user', array('id' => $userid));
forum_cron_minimise_user_record($userto);
}
$userto->viewfullnames = array();
$userto->canpost = array();
$userto->markposts = array();
// Override the language and timezone of the "current" user, so that
// mail is customised for the receiver.
cron_setup_user($userto);
$postsubject = get_string('digestmailsubject', 'forum', format_string($site->shortname, true));
$headerdata = new stdClass();
$headerdata->sitename = format_string($site->fullname, true);
$headerdata->userprefs = $CFG->wwwroot.'/user/forum.php?id='.$userid.'&amp;course='.$site->id;
$posttext = get_string('digestmailheader', 'forum', $headerdata)."\n\n";
$headerdata->userprefs = '<a target="_blank" href="'.$headerdata->userprefs.'">'.get_string('digestmailprefs', 'forum').'</a>';
$posthtml = '<p>'.get_string('digestmailheader', 'forum', $headerdata).'</p>'
. '<br /><hr size="1" noshade="noshade" />';
foreach ($thesediscussions as $discussionid) {
core_php_time_limit::raise(120); // to be reset for each post
$discussion = $discussions[$discussionid];
$forum = $forums[$discussion->forum];
$course = $courses[$forum->course];
$cm = $coursemodules[$forum->id];
//override language
cron_setup_user($userto, $course);
// Fill caches
if (!isset($userto->viewfullnames[$forum->id])) {
$modcontext = context_module::instance($cm->id);
$userto->viewfullnames[$forum->id] = has_capability('moodle/site:viewfullnames', $modcontext);
}
if (!isset($userto->canpost[$discussion->id])) {
$modcontext = context_module::instance($cm->id);
$userto->canpost[$discussion->id] = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
}
$strforums = get_string('forums', 'forum');
$canunsubscribe = ! \mod_forum\subscriptions::is_forcesubscribed($forum);
$canreply = $userto->canpost[$discussion->id];
$shortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
$posttext .= "\n \n";
$posttext .= '=====================================================================';
$posttext .= "\n \n";
$posttext .= "$shortname -> $strforums -> ".format_string($forum->name,true);
if ($discussion->name != $forum->name) {
$posttext .= " -> ".format_string($discussion->name,true);
}
$posttext .= "\n";
$posttext .= $CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->id;
$posttext .= "\n";
$posthtml .= "<p><font face=\"sans-serif\">".
"<a target=\"_blank\" href=\"$CFG->wwwroot/course/view.php?id=$course->id\">$shortname</a> -> ".
"<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/index.php?id=$course->id\">$strforums</a> -> ".
"<a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/view.php?f=$forum->id\">".format_string($forum->name,true)."</a>";
if ($discussion->name == $forum->name) {
$posthtml .= "</font></p>";
} else {
$posthtml .= " -> <a target=\"_blank\" href=\"$CFG->wwwroot/mod/forum/discuss.php?d=$discussion->id\">".format_string($discussion->name,true)."</a></font></p>";
}
$posthtml .= '<p>';
$postsarray = $discussionposts[$discussionid];
sort($postsarray);
$sentcount = 0;
foreach ($postsarray as $postid) {
$post = $posts[$postid];
if (array_key_exists($post->userid, $users)) { // we might know him/her already
$userfrom = $users[$post->userid];
if (!isset($userfrom->idnumber)) {
$userfrom = $DB->get_record('user', array('id' => $userfrom->id));
forum_cron_minimise_user_record($userfrom);
}
} else if ($userfrom = $DB->get_record('user', array('id' => $post->userid))) {
forum_cron_minimise_user_record($userfrom);
if ($userscount <= FORUM_CRON_USER_CACHE) {
$userscount++;
$users[$userfrom->id] = $userfrom;
}
} else {
mtrace('Could not find user '.$post->userid);
continue;
}
if (!isset($userfrom->groups[$forum->id])) {
if (!isset($userfrom->groups)) {
$userfrom->groups = array();
if (isset($users[$userfrom->id])) {
$users[$userfrom->id]->groups = array();
}
}
$userfrom->groups[$forum->id] = groups_get_all_groups($course->id, $userfrom->id, $cm->groupingid);
if (isset($users[$userfrom->id])) {
$users[$userfrom->id]->groups[$forum->id] = $userfrom->groups[$forum->id];
}
}
// Headers to help prevent auto-responders.
$userfrom->customheaders = array(
"Precedence: Bulk",
'X-Auto-Response-Suppress: All',
'Auto-Submitted: auto-generated',
);
$maildigest = forum_get_user_maildigest_bulk($digests, $userto, $forum->id);
if (!isset($userto->canpost[$discussion->id])) {
$canreply = forum_user_can_post($forum, $discussion, $userto, $cm, $course, $modcontext);
} else {
$canreply = $userto->canpost[$discussion->id];
}
$data = new \mod_forum\output\forum_post_email(
$course,
$cm,
$forum,
$discussion,
$post,
$userfrom,
$userto,
$canreply
);
if (!isset($userto->viewfullnames[$forum->id])) {
$data->viewfullnames = has_capability('moodle/site:viewfullnames', $modcontext, $userto->id);
} else {
$data->viewfullnames = $userto->viewfullnames[$forum->id];
}
if ($maildigest == 2) {
// Subjects and link only.
$posttext .= $textdigestbasicout->render($data);
$posthtml .= $htmldigestbasicout->render($data);
} else {
// The full treatment.
$posttext .= $textdigestfullout->render($data);
$posthtml .= $htmldigestfullout->render($data);
// Create an array of postid's for this user to mark as read.
if (!$CFG->forum_usermarksread) {
$userto->markposts[$post->id] = $post->id;
}
}
$sentcount++;
}
$footerlinks = array();
if ($canunsubscribe) {
$footerlinks[] = "<a href=\"$CFG->wwwroot/mod/forum/subscribe.php?id=$forum->id\">" . get_string("unsubscribe", "forum") . "</a>";
} else {
$footerlinks[] = get_string("everyoneissubscribed", "forum");
}
$footerlinks[] = "<a href='{$CFG->wwwroot}/mod/forum/index.php?id={$forum->course}'>" . get_string("digestmailpost", "forum") . '</a>';
$posthtml .= "\n<div class='mdl-right'><font size=\"1\">" . implode('&nbsp;', $footerlinks) . '</font></div>';
$posthtml .= '<hr size="1" noshade="noshade" /></p>';
}
if (empty($userto->mailformat) || $userto->mailformat != 1) {
// This user DOESN'T want to receive HTML
$posthtml = '';
}
$eventdata = new \core\message\message();
$eventdata->courseid = SITEID;
$eventdata->component = 'mod_forum';
$eventdata->name = 'digests';
$eventdata->userfrom = core_user::get_noreply_user();
$eventdata->userto = $userto;
$eventdata->subject = $postsubject;
$eventdata->fullmessage = $posttext;
$eventdata->fullmessageformat = FORMAT_PLAIN;
$eventdata->fullmessagehtml = $posthtml;
$eventdata->notification = 1;
$eventdata->smallmessage = get_string('smallmessagedigest', 'forum', $sentcount);
$mailresult = message_send($eventdata);
if (!$mailresult) {
mtrace("ERROR: mod/forum/cron.php: Could not send out digest mail to user $userto->id ".
"($userto->email)... not trying again.");
} else {
mtrace("success.");
$usermailcount++;
// Mark post as read if forum_usermarksread is set off
if (get_user_preferences('forum_markasreadonnotification', 1, $userto->id) == 1) {
forum_tp_mark_posts_read($userto, $userto->markposts);
}
}
}
}
/// We have finishied all digest emails, update $CFG->digestmailtimelast
set_config('digestmailtimelast', $timenow);
}
cron_setup_user();
if (!empty($usermailcount)) {
mtrace(get_string('digestsentusers', 'forum', $usermailcount));
}
if (!empty($CFG->forum_lastreadclean)) {
$timenow = time();
if ($CFG->forum_lastreadclean + (24*3600) < $timenow) {
set_config('forum_lastreadclean', $timenow);
mtrace('Removing old forum read tracking info...');
forum_tp_clean_read_records();
}
} else {
set_config('forum_lastreadclean', time());
}
return true;
}
/**
*
* @param object $course
@@ -2161,86 +1338,6 @@ function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=5
return $DB->get_records_sql($searchsql, $params, $limitfrom, $limitnum);
}
/**
* Returns a list of all new posts that have not been mailed yet
*
* @param int $starttime posts created after this time
* @param int $endtime posts created before this
* @param int $now used for timed discussions only
* @return array
*/
function forum_get_unmailed_posts($starttime, $endtime, $now=null) {
global $CFG, $DB;
$params = array();
$params['mailed'] = FORUM_MAILED_PENDING;
$params['ptimestart'] = $starttime;
$params['ptimeend'] = $endtime;
$params['mailnow'] = 1;
if (!empty($CFG->forum_enabletimedposts)) {
if (empty($now)) {
$now = time();
}
$selectsql = "AND (p.created >= :ptimestart OR d.timestart >= :pptimestart)";
$params['pptimestart'] = $starttime;
$timedsql = "AND (d.timestart < :dtimestart AND (d.timeend = 0 OR d.timeend > :dtimeend))";
$params['dtimestart'] = $now;
$params['dtimeend'] = $now;
} else {
$timedsql = "";
$selectsql = "AND p.created >= :ptimestart";
}
return $DB->get_records_sql("SELECT p.*, d.course, d.forum
FROM {forum_posts} p
JOIN {forum_discussions} d ON d.id = p.discussion
WHERE p.mailed = :mailed
$selectsql
AND (p.created < :ptimeend OR p.mailnow = :mailnow)
$timedsql
ORDER BY p.modified ASC", $params);
}
/**
* Marks posts before a certain time as being mailed already
*
* @global object
* @global object
* @param int $endtime
* @param int $now Defaults to time()
* @return bool
*/
function forum_mark_old_posts_as_mailed($endtime, $now=null) {
global $CFG, $DB;
if (empty($now)) {
$now = time();
}
$params = array();
$params['mailedsuccess'] = FORUM_MAILED_SUCCESS;
$params['now'] = $now;
$params['endtime'] = $endtime;
$params['mailnow'] = 1;
$params['mailedpending'] = FORUM_MAILED_PENDING;
if (empty($CFG->forum_enabletimedposts)) {
return $DB->execute("UPDATE {forum_posts}
SET mailed = :mailedsuccess
WHERE (created < :endtime OR mailnow = :mailnow)
AND mailed = :mailedpending", $params);
} else {
return $DB->execute("UPDATE {forum_posts}
SET mailed = :mailedsuccess
WHERE discussion NOT IN (SELECT d.id
FROM {forum_discussions} d
WHERE d.timestart > :now)
AND (created < :endtime OR mailnow = :mailnow)
AND mailed = :mailedpending", $params);
}
}
/**
* Get all the posts for a user in a forum suitable for forum_print_post
*
@@ -44,7 +44,8 @@
} {{/ str }}
---------------------------------------------------------------------
{{{ message }}}
{{# attachments }}
{{{ attachments }}}
{{/ attachments }}
---------------------------------------------------------------------
{{# str }} digestmailpostlink, forum, {{{ forumindexlink }}} {{/ str }}
+129
View File
@@ -0,0 +1,129 @@
<?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/>.
/**
* The forum module cron trait.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
trait mod_forum_tests_cron_trait {
/**
* Run the main cron task to queue all tasks, and ensure that posts
* were sent to the correct users.
*
* @param \stdClass[] $expectations The list of users, along with their expected count of messages and digests.
*/
protected function queue_tasks_and_assert($expectations = []) {
global $DB;
// Note, we cannot use expectOutputRegex because it only allows for a single RegExp.
ob_start();
cron_setup_user();
$cron = new \mod_forum\task\cron_task();
$cron->execute();
$output = ob_get_contents();
ob_end_clean();
$uniqueusers = 0;
foreach ($expectations as $expect) {
$expect->digests = isset($expect->digests) ? $expect->digests : 0;
$expect->messages = isset($expect->messages) ? $expect->messages : 0;
$expect->mentioned = isset($expect->mentioned) ? $expect->mentioned : false;
if ($expect->digests || $expect->messages) {
$expect->mentioned = true;
}
if (!$expect->mentioned) {
$this->assertNotRegExp("/Queued 0 for {$expect->userid}/", $output);
} else {
$uniqueusers++;
$this->assertRegExp(
"/Queued {$expect->digests} digests and {$expect->messages} messages for {$expect->userid}/",
$output
);
}
}
if (empty($expectations)) {
$this->assertRegExp("/No posts found./", $output);
} else {
$this->assertRegExp("/Unique users: {$uniqueusers}/", $output);
}
// Update the forum queue for digests.
$DB->execute("UPDATE {forum_queue} SET timemodified = timemodified - 1");
}
/**
* Run any send_user_notifications tasks for the specified user, and
* ensure that the posts specified were sent.
*
* @param \stdClass $user
* @param \stdClass[] $posts
* @param bool $ignoreemptyposts
*/
protected function send_notifications_and_assert($user, $posts = [], $ignoreemptyposts = false) {
ob_start();
$this->runAdhocTasks(\mod_forum\task\send_user_notifications::class, $user->id);
$output = ob_get_contents();
ob_end_clean();
if (empty($posts) && !$ignoreemptyposts) {
$this->assertEquals('', $output);
} else {
$this->assertRegExp("/Sending messages to {$user->username}/", $output);
foreach ($posts as $post) {
$this->assertRegExp("/Post {$post->id} sent/", $output);
}
$count = count($posts);
$this->assertRegExp("/Sent {$count} messages with 0 failures/", $output);
}
}
/**
* Run any send_user_digests tasks for the specified user, and
* ensure that the posts specified were sent.
*
* @param \stdClass $user
* @param \stdClass[] $fullposts
* @param \stdClass[] $shortposts
*/
protected function send_digests_and_assert($user, $fullposts = [], $shortposts = []) {
ob_start();
$this->runAdhocTasks(\mod_forum\task\send_user_digests::class, $user->id);
$output = ob_get_contents();
ob_end_clean();
if (empty($shortposts) && empty($fullposts)) {
$this->assertEquals('', $output);
$this->assertRegExp("/Digest sent with 0 messages./", $output);
} else {
$this->assertRegExp("/Sending forum digests for {$user->username}/", $output);
foreach ($fullposts as $post) {
$this->assertRegExp("/Adding post {$post->id} in format 1/", $output);
}
foreach ($shortposts as $post) {
$this->assertRegExp("/Adding post {$post->id} in format 2/", $output);
}
$count = count($fullposts) + count($shortposts);
$this->assertRegExp("/Digest sent with {$count} messages./", $output);
}
}
}
+159
View File
@@ -0,0 +1,159 @@
<?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/>.
/**
* The forum module trait with additional generator helpers.
*
* @package mod_forum
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
trait mod_forum_tests_generator_trait {
/**
* Helper to create the required number of users in the specified course.
* Users are enrolled as students by default.
*
* @param stdClass $course The course object
* @param integer $count The number of users to create
* @param string $role The role to assign users as
* @return array The users created
*/
protected function helper_create_users($course, $count, $role = null) {
$users = array();
for ($i = 0; $i < $count; $i++) {
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user($user->id, $course->id, $role);
$users[] = $user;
}
return $users;
}
/**
* Create a new discussion and post within the specified forum, as the
* specified author.
*
* @param stdClass $forum The forum to post in
* @param stdClass $author The author to post as
* @param array $fields any other fields in discussion (name, message, messageformat, ...)
* @return array An array containing the discussion object, and the post object
*/
protected function helper_post_to_forum($forum, $author, $fields = array()) {
global $DB;
$generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
// Create a discussion in the forum, and then add a post to that discussion.
$record = (object)$fields;
$record->course = $forum->course;
$record->userid = $author->id;
$record->forum = $forum->id;
$discussion = $generator->create_discussion($record);
// Retrieve the post which was created by create_discussion.
$post = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
return array($discussion, $post);
}
/**
* Update the post time for the specified post by $factor.
*
* @param stdClass $post The post to update
* @param int $factor The amount to update by
*/
protected function helper_update_post_time($post, $factor) {
global $DB;
// Update the post to have a created in the past.
$DB->set_field('forum_posts', 'created', $post->created + $factor, array('id' => $post->id));
}
/**
* Update the subscription time for the specified user/discussion by $factor.
*
* @param stdClass $user The user to update
* @param stdClass $discussion The discussion to update for this user
* @param int $factor The amount to update by
*/
protected function helper_update_subscription_time($user, $discussion, $factor) {
global $DB;
$sub = $DB->get_record('forum_discussion_subs', array('userid' => $user->id, 'discussion' => $discussion->id));
// Update the subscription to have a preference in the past.
$DB->set_field('forum_discussion_subs', 'preference', $sub->preference + $factor, array('id' => $sub->id));
}
/**
* Create a new post within an existing discussion, as the specified author.
*
* @param stdClass $forum The forum to post in
* @param stdClass $discussion The discussion to post in
* @param stdClass $author The author to post as
* @return stdClass The forum post
*/
protected function helper_post_to_discussion($forum, $discussion, $author) {
global $DB;
$generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
// Add a post to the discussion.
$record = new stdClass();
$record->course = $forum->course;
$strre = get_string('re', 'forum');
$record->subject = $strre . ' ' . $discussion->subject;
$record->userid = $author->id;
$record->forum = $forum->id;
$record->discussion = $discussion->id;
$record->mailnow = 1;
$post = $generator->create_post($record);
return $post;
}
/**
* Create a new post within an existing discussion, as the specified author.
*
* @param stdClass $parent The post being replied to
* @param stdClass $author The author to post as
* @return stdClass The forum post
*/
protected function helper_reply_to_post($parent, $author) {
global $DB;
$generator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
// Add a post to the discussion.
$strre = get_string('re', 'forum');
$record = (object) [
'discussion' => $parent->discussion,
'parent' => $parent->id,
'userid' => $author->id,
'mailnow' => 1,
'subject' => $strre . ' ' . $parent->subject,
];
$post = $generator->create_post($record);
return $post;
}
}
-216
View File
@@ -2961,65 +2961,6 @@ class mod_forum_lib_testcase extends advanced_testcase {
$this->assertEmpty($neighbours['next']);
}
/**
* @dataProvider forum_get_unmailed_posts_provider
*/
public function test_forum_get_unmailed_posts($discussiondata, $enabletimedposts, $expectedcount, $expectedreplycount) {
global $CFG, $DB;
$this->resetAfterTest();
// Configure timed posts.
$CFG->forum_enabletimedposts = $enabletimedposts;
$course = $this->getDataGenerator()->create_course();
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
$user = $this->getDataGenerator()->create_user();
$forumgen = $this->getDataGenerator()->get_plugin_generator('mod_forum');
// Keep track of the start time of the test. Do not use time() after this point to prevent random failures.
$time = time();
$record = new stdClass();
$record->course = $course->id;
$record->userid = $user->id;
$record->forum = $forum->id;
if (isset($discussiondata['timecreated'])) {
$record->timemodified = $time + $discussiondata['timecreated'];
}
if (isset($discussiondata['timestart'])) {
$record->timestart = $time + $discussiondata['timestart'];
}
if (isset($discussiondata['timeend'])) {
$record->timeend = $time + $discussiondata['timeend'];
}
if (isset($discussiondata['mailed'])) {
$record->mailed = $discussiondata['mailed'];
}
$discussion = $forumgen->create_discussion($record);
// Fetch the unmailed posts.
$timenow = $time;
$endtime = $timenow - $CFG->maxeditingtime;
$starttime = $endtime - 2 * DAYSECS;
$unmailed = forum_get_unmailed_posts($starttime, $endtime, $timenow);
$this->assertCount($expectedcount, $unmailed);
// Add a reply just outside the maxeditingtime.
$replyto = $DB->get_record('forum_posts', array('discussion' => $discussion->id));
$reply = new stdClass();
$reply->userid = $user->id;
$reply->discussion = $discussion->id;
$reply->parent = $replyto->id;
$reply->created = max($replyto->created, $endtime - 1);
$forumgen->create_post($reply);
$unmailed = forum_get_unmailed_posts($starttime, $endtime, $timenow);
$this->assertCount($expectedreplycount, $unmailed);
}
/**
* Test for forum_is_author_hidden.
*/
@@ -3055,163 +2996,6 @@ class mod_forum_lib_testcase extends advanced_testcase {
forum_is_author_hidden($post, $forum);
}
public function forum_get_unmailed_posts_provider() {
return [
'Untimed discussion; Single post; maxeditingtime not expired' => [
'discussion' => [
],
'timedposts' => false,
'postcount' => 0,
'replycount' => 0,
],
'Untimed discussion; Single post; maxeditingtime expired' => [
'discussion' => [
'timecreated' => - DAYSECS,
],
'timedposts' => false,
'postcount' => 1,
'replycount' => 2,
],
'Timed discussion; Single post; Posted 1 week ago; timestart maxeditingtime not expired' => [
'discussion' => [
'timecreated' => - WEEKSECS,
'timestart' => 0,
],
'timedposts' => true,
'postcount' => 0,
'replycount' => 0,
],
'Timed discussion; Single post; Posted 1 week ago; timestart maxeditingtime expired' => [
'discussion' => [
'timecreated' => - WEEKSECS,
'timestart' => - DAYSECS,
],
'timedposts' => true,
'postcount' => 1,
'replycount' => 2,
],
'Timed discussion; Single post; Posted 1 week ago; timestart maxeditingtime expired; timeend not reached' => [
'discussion' => [
'timecreated' => - WEEKSECS,
'timestart' => - DAYSECS,
'timeend' => + DAYSECS
],
'timedposts' => true,
'postcount' => 1,
'replycount' => 2,
],
'Timed discussion; Single post; Posted 1 week ago; timestart maxeditingtime expired; timeend passed' => [
'discussion' => [
'timecreated' => - WEEKSECS,
'timestart' => - DAYSECS,
'timeend' => - HOURSECS,
],
'timedposts' => true,
'postcount' => 0,
'replycount' => 0,
],
'Timed discussion; Single post; Posted 1 week ago; timeend not reached' => [
'discussion' => [
'timecreated' => - WEEKSECS,
'timeend' => + DAYSECS
],
'timedposts' => true,
'postcount' => 0,
'replycount' => 1,
],
'Timed discussion; Single post; Posted 1 week ago; timeend passed' => [
'discussion' => [
'timecreated' => - WEEKSECS,
'timeend' => - DAYSECS,
],
'timedposts' => true,
'postcount' => 0,
'replycount' => 0,
],
'Previously mailed; Untimed discussion; Single post; maxeditingtime not expired' => [
'discussion' => [
'mailed' => 1,
],
'timedposts' => false,
'postcount' => 0,
'replycount' => 0,
],
'Previously mailed; Untimed discussion; Single post; maxeditingtime expired' => [
'discussion' => [
'timecreated' => - DAYSECS,
'mailed' => 1,
],
'timedposts' => false,
'postcount' => 0,
'replycount' => 1,
],
'Previously mailed; Timed discussion; Single post; Posted 1 week ago; timestart maxeditingtime not expired' => [
'discussion' => [
'timecreated' => - WEEKSECS,
'timestart' => 0,
'mailed' => 1,
],
'timedposts' => true,
'postcount' => 0,
'replycount' => 0,
],
'Previously mailed; Timed discussion; Single post; Posted 1 week ago; timestart maxeditingtime expired' => [
'discussion' => [
'timecreated' => - WEEKSECS,
'timestart' => - DAYSECS,
'mailed' => 1,
],
'timedposts' => true,
'postcount' => 0,
'replycount' => 1,
],
'Previously mailed; Timed discussion; Single post; Posted 1 week ago; timestart maxeditingtime expired; timeend not reached' => [
'discussion' => [
'timecreated' => - WEEKSECS,
'timestart' => - DAYSECS,
'timeend' => + DAYSECS,
'mailed' => 1,
],
'timedposts' => true,
'postcount' => 0,
'replycount' => 1,
],
'Previously mailed; Timed discussion; Single post; Posted 1 week ago; timestart maxeditingtime expired; timeend passed' => [
'discussion' => [
'timecreated' => - WEEKSECS,
'timestart' => - DAYSECS,
'timeend' => - HOURSECS,
'mailed' => 1,
],
'timedposts' => true,
'postcount' => 0,
'replycount' => 0,
],
'Previously mailed; Timed discussion; Single post; Posted 1 week ago; timeend not reached' => [
'discussion' => [
'timecreated' => - WEEKSECS,
'timeend' => + DAYSECS,
'mailed' => 1,
],
'timedposts' => true,
'postcount' => 0,
'replycount' => 1,
],
'Previously mailed; Timed discussion; Single post; Posted 1 week ago; timeend passed' => [
'discussion' => [
'timecreated' => - WEEKSECS,
'timeend' => - DAYSECS,
'mailed' => 1,
],
'timedposts' => true,
'postcount' => 0,
'replycount' => 0,
],
];
}
/**
* Test the forum_discussion_is_locked function.
*
+246
View File
@@ -0,0 +1,246 @@
<?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/>.
/**
* The forum module mail generation tests for groups.
*
* @package mod_forum
* @copyright 2013 Andrew Nicols
* @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(__DIR__ . '/cron_trait.php');
require_once(__DIR__ . '/generator_trait.php');
/**
* The forum module mail generation tests for groups.
*
* @copyright 2013 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_forum_mail_group_testcase extends advanced_testcase {
// Make use of the cron tester trait.
use mod_forum_tests_cron_trait;
// Make use of the test generator trait.
use mod_forum_tests_generator_trait;
/**
* @var \phpunit_message_sink
*/
protected $messagesink;
/**
* @var \phpunit_mailer_sink
*/
protected $mailsink;
public function setUp() {
global $CFG;
// 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();
\mod_forum\subscriptions::reset_discussion_cache();
// Messaging is not compatible with transactions...
$this->preventResetByRollback();
// Catch all messages.
$this->messagesink = $this->redirectMessages();
$this->mailsink = $this->redirectEmails();
// Forcibly reduce the maxeditingtime to a second in the past to
// ensure that messages are sent out.
$CFG->maxeditingtime = -1;
}
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();
$this->messagesink->clear();
$this->messagesink->close();
unset($this->messagesink);
$this->mailsink->clear();
$this->mailsink->close();
unset($this->mailsink);
}
/**
* Ensure that posts written in a forum marked for separate groups includes notifications for the members of that
* group, and any user with accessallgroups.
*/
public function test_separate_group() {
global $CFG, $DB;
$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,
'groupmode' => SEPARATEGROUPS,
]);
// Create three students:
// - author, enrolled in group A; and
// - recipient, enrolled in group B; and
// - other, enrolled in the course, but no groups.
list($author, $recipient, $otheruser) = $this->helper_create_users($course, 3);
// Create one teacher, not in any group and no accessallgroups capability.
list($teacher) = $this->helper_create_users($course, 1, 'teacher');
// Create one editing teacher, not in any group but with accessallgroups capability.
list($editingteacher) = $this->helper_create_users($course, 1, 'editingteacher');
$groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$groupb = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$this->getDataGenerator()->create_group_member([
'groupid' => $groupa->id,
'userid' => $author->id,
]);
$this->getDataGenerator()->create_group_member([
'groupid' => $groupb->id,
'userid' => $recipient->id,
]);
// Post a discussion to the forum.
list($discussion, $post) = $this->helper_post_to_forum($forum, $author, [
'groupid' => $groupa->id,
]);
// Only the author should receive.
$expect = [
'author' => (object) [
'userid' => $author->id,
'messages' => 1,
],
'recipient' => (object) [
'userid' => $recipient->id,
'messages' => 0,
],
'otheruser' => (object) [
'userid' => $otheruser->id,
'messages' => 0,
],
'teacher' => (object) [
'userid' => $teacher->id,
'messages' => 0,
],
'editingteacher' => (object) [
'userid' => $editingteacher->id,
'messages' => 1,
],
];
$this->queue_tasks_and_assert($expect);
// No notifications should be queued.
$this->send_notifications_and_assert($author, [$post]);
$this->send_notifications_and_assert($recipient, []);
$this->send_notifications_and_assert($otheruser, []);
$this->send_notifications_and_assert($teacher, []);
$this->send_notifications_and_assert($editingteacher, [$post]);
}
/**
* Ensure that posts written in a forum marked for visible groups includes notifications for the members of that
* group, and any user with accessallgroups.
*/
public function test_visible_group() {
global $CFG, $DB;
$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,
'groupmode' => VISIBLEGROUPS,
]);
// Create three students:
// - author, enrolled in group A; and
// - recipient, enrolled in group B; and
// - other, enrolled in the course, but no groups.
list($author, $recipient, $otheruser) = $this->helper_create_users($course, 3);
// Create one teacher, not in any group and no accessallgroups capability.
list($teacher) = $this->helper_create_users($course, 1, 'teacher');
// Create one editing teacher, not in any group but with accessallgroups capability.
list($editingteacher) = $this->helper_create_users($course, 1, 'editingteacher');
$groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$groupb = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
$this->getDataGenerator()->create_group_member([
'groupid' => $groupa->id,
'userid' => $author->id,
]);
$this->getDataGenerator()->create_group_member([
'groupid' => $groupb->id,
'userid' => $recipient->id,
]);
// Post a discussion to the forum.
list($discussion, $post) = $this->helper_post_to_forum($forum, $author, [
'groupid' => $groupa->id,
]);
// Only the author should receive.
$expect = [
'author' => (object) [
'userid' => $author->id,
'messages' => 1,
],
'recipient' => (object) [
'userid' => $recipient->id,
'messages' => 0,
],
'otheruser' => (object) [
'userid' => $otheruser->id,
'messages' => 0,
],
'teacher' => (object) [
'userid' => $teacher->id,
'messages' => 0,
],
'editingteacher' => (object) [
'userid' => $editingteacher->id,
'messages' => 1,
],
];
$this->queue_tasks_and_assert($expect);
// No notifications should be queued.
$this->send_notifications_and_assert($author, [$post]);
$this->send_notifications_and_assert($recipient, []);
$this->send_notifications_and_assert($otheruser, []);
$this->send_notifications_and_assert($teacher, []);
$this->send_notifications_and_assert($editingteacher, [$post]);
}
}
File diff suppressed because it is too large Load Diff
+97 -139
View File
@@ -26,17 +26,16 @@
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once(__DIR__ . '/cron_trait.php');
require_once(__DIR__ . '/generator_trait.php');
class mod_forum_maildigest_testcase extends advanced_testcase {
/**
* Keep track of the message and mail sinks that we set up for each
* test.
*
* @var stdClass $helper
*/
protected $helper;
// Make use of the cron tester trait.
use mod_forum_tests_cron_trait;
// Make use of the test generator trait.
use mod_forum_tests_generator_trait;
/**
* Set up message and mail sinks, and set up other requirements for the
@@ -45,20 +44,18 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
public function setUp() {
global $CFG;
$this->helper = new stdClass();
// Messaging is not compatible with transactions...
$this->preventResetByRollback();
// Catch all messages
$this->helper->messagesink = $this->redirectMessages();
$this->helper->mailsink = $this->redirectEmails();
$this->messagesink = $this->redirectMessages();
$this->mailsink = $this->redirectEmails();
// Confirm that we have an empty message sink so far.
$messages = $this->helper->messagesink->get_messages();
$messages = $this->messagesink->get_messages();
$this->assertEquals(0, count($messages));
$messages = $this->helper->mailsink->get_messages();
$messages = $this->mailsink->get_messages();
$this->assertEquals(0, count($messages));
// Tell Moodle that we've not sent any digest messages out recently.
@@ -82,11 +79,11 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
* Clear the message sinks set up in this test.
*/
public function tearDown() {
$this->helper->messagesink->clear();
$this->helper->messagesink->close();
$this->messagesink->clear();
$this->messagesink->close();
$this->helper->mailsink->clear();
$this->helper->mailsink->close();
$this->mailsink->clear();
$this->mailsink->close();
}
/**
@@ -131,52 +128,6 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
return $return;
}
/**
* Helper to falsify all forum post records for a digest run.
*/
protected function helper_force_digest_mail_times() {
global $CFG, $DB;
// Fake all of the post editing times because digests aren't sent until
// the start of an hour where the modification time on the message is before
// the start of that hour
$sitetimezone = core_date::get_server_timezone();
$digesttime = usergetmidnight(time(), $sitetimezone) + ($CFG->digestmailtime * 3600) - (60 * 60);
$DB->set_field('forum_posts', 'modified', $digesttime, array('mailed' => 0));
$DB->set_field('forum_posts', 'created', $digesttime, array('mailed' => 0));
}
/**
* Run the forum cron, and check that the specified post was sent the
* specified number of times.
*
* @param integer $expected The number of times that the post should have been sent
* @param integer $individualcount The number of individual messages sent
* @param integer $digestcount The number of digest messages sent
*/
protected function helper_run_cron_check_count($expected, $individualcount, $digestcount) {
if ($expected === 0) {
$this->expectOutputRegex('/(Email digests successfully sent to .* users.){0}/');
} else {
$this->expectOutputRegex("/Email digests successfully sent to {$expected} users/");
}
forum_cron();
// Now check the results in the message sink.
$messages = $this->helper->messagesink->get_messages();
$counts = (object) array('digest' => 0, 'individual' => 0);
foreach ($messages as $message) {
if (strpos($message->subject, 'forum digest') !== false) {
$counts->digest++;
} else {
$counts->individual++;
}
}
$this->assertEquals($digestcount, $counts->digest);
$this->assertEquals($individualcount, $counts->individual);
}
public function test_set_maildigest() {
global $DB;
@@ -302,10 +253,9 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
$this->resetAfterTest(true);
$this->helper_force_digest_mail_times();
// Initially the forum cron should generate no messages as we've made no posts.
$this->helper_run_cron_check_count(0, 0, 0);
$expect = [];
$this->queue_tasks_and_assert($expect);
}
/**
@@ -324,27 +274,19 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
$forum1 = $userhelper->forums->forum1;
$forum2 = $userhelper->forums->forum2;
// Add some discussions to the forums.
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user->id;
$record->mailnow = 1;
// Add 5 discussions to forum 1.
$record->forum = $forum1->id;
$posts = [];
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]);
$posts[] = $post;
}
// Add 5 discussions to forum 2.
$record->forum = $forum2->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]);
$posts[] = $post;
}
// Ensure that the creation times mean that the messages will be sent.
$this->helper_force_digest_mail_times();
// Set the tested user's default maildigest setting.
$DB->set_field('user', 'maildigest', 0, array('id' => $user->id));
@@ -355,7 +297,16 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
forum_set_user_maildigest($forum2, -1, $user);
// No digests mails should be sent, but 10 forum mails will be sent.
$this->helper_run_cron_check_count(0, 10, 0);
$expect = [
(object) [
'userid' => $user->id,
'messages' => 10,
'digests' => 0,
],
];
$this->queue_tasks_and_assert($expect);
$this->send_notifications_and_assert($user, $posts);
}
/**
@@ -373,28 +324,20 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
$course1 = $userhelper->courses->course1;
$forum1 = $userhelper->forums->forum1;
$forum2 = $userhelper->forums->forum2;
// Add a discussion to the forums.
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user->id;
$record->mailnow = 1;
$posts = [];
// Add 5 discussions to forum 1.
$record->forum = $forum1->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]);
$posts[] = $post;
}
// Add 5 discussions to forum 2.
$record->forum = $forum2->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]);
$posts[] = $post;
}
// Ensure that the creation times mean that the messages will be sent.
$this->helper_force_digest_mail_times();
// Set the tested user's default maildigest setting.
$DB->set_field('user', 'maildigest', 1, array('id' => $user->id));
@@ -404,8 +347,17 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
// Set the maildigest preference for forum2 to default.
forum_set_user_maildigest($forum2, -1, $user);
// One digest mail should be sent, with no notifications, and one e-mail.
$this->helper_run_cron_check_count(1, 0, 1);
// No digests mails should be sent, but 10 forum mails will be sent.
$expect = [
(object) [
'userid' => $user->id,
'messages' => 0,
'digests' => 1,
],
];
$this->queue_tasks_and_assert($expect);
$this->send_digests_and_assert($user, $posts);
}
/**
@@ -424,28 +376,21 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
$course1 = $userhelper->courses->course1;
$forum1 = $userhelper->forums->forum1;
$forum2 = $userhelper->forums->forum2;
// Add a discussion to the forums.
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user->id;
$record->mailnow = 1;
$posts = [];
$digests = [];
// Add 5 discussions to forum 1.
$record->forum = $forum1->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]);
$digests[] = $post;
}
// Add 5 discussions to forum 2.
$record->forum = $forum2->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]);
$posts[] = $post;
}
// Ensure that the creation times mean that the messages will be sent.
$this->helper_force_digest_mail_times();
// Set the tested user's default maildigest setting.
$DB->set_field('user', 'maildigest', 0, array('id' => $user->id));
@@ -456,7 +401,17 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
forum_set_user_maildigest($forum2, -1, $user);
// One digest e-mail should be sent, and five individual notifications.
$this->helper_run_cron_check_count(1, 5, 1);
$expect = [
(object) [
'userid' => $user->id,
'messages' => 5,
'digests' => 1,
],
];
$this->queue_tasks_and_assert($expect);
$this->send_notifications_and_assert($user, $posts);
$this->send_digests_and_assert($user, $digests);
}
/**
@@ -475,28 +430,21 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
$course1 = $userhelper->courses->course1;
$forum1 = $userhelper->forums->forum1;
$forum2 = $userhelper->forums->forum2;
// Add a discussion to the forums.
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user->id;
$record->mailnow = 1;
$posts = [];
$digests = [];
// Add 5 discussions to forum 1.
$record->forum = $forum1->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]);
$digests[] = $post;
}
// Add 5 discussions to forum 2.
$record->forum = $forum2->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]);
$posts[] = $post;
}
// Ensure that the creation times mean that the messages will be sent.
$this->helper_force_digest_mail_times();
// Set the tested user's default maildigest setting.
$DB->set_field('user', 'maildigest', 1, array('id' => $user->id));
@@ -507,7 +455,17 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
forum_set_user_maildigest($forum2, 0, $user);
// One digest e-mail should be sent, and five individual notifications.
$this->helper_run_cron_check_count(1, 5, 1);
$expect = [
(object) [
'userid' => $user->id,
'messages' => 5,
'digests' => 1,
],
];
$this->queue_tasks_and_assert($expect);
$this->send_notifications_and_assert($user, $posts);
$this->send_digests_and_assert($user, $digests);
}
/**
@@ -525,28 +483,21 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
$course1 = $userhelper->courses->course1;
$forum1 = $userhelper->forums->forum1;
$forum2 = $userhelper->forums->forum2;
// Add a discussion to the forums.
$record = new stdClass();
$record->course = $course1->id;
$record->userid = $user->id;
$record->mailnow = 1;
$fulldigests = [];
$shortdigests = [];
// Add 5 discussions to forum 1.
$record->forum = $forum1->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum1, $user, ['mailnow' => 1]);
$fulldigests[] = $post;
}
// Add 5 discussions to forum 2.
$record->forum = $forum2->id;
for ($i = 0; $i < 5; $i++) {
$this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
list($discussion, $post) = $this->helper_post_to_forum($forum2, $user, ['mailnow' => 1]);
$shortdigests[] = $post;
}
// Ensure that the creation times mean that the messages will be sent.
$this->helper_force_digest_mail_times();
// Set the tested user's default maildigest setting.
$DB->set_field('user', 'maildigest', 0, array('id' => $user->id));
@@ -557,7 +508,14 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
forum_set_user_maildigest($forum2, 2, $user);
// One digest e-mail should be sent, and no individual notifications.
$this->helper_run_cron_check_count(1, 0, 1);
}
$expect = [
(object) [
'userid' => $user->id,
'digests' => 1,
],
];
$this->queue_tasks_and_assert($expect);
$this->send_digests_and_assert($user, $fulldigests, $shortdigests);
}
}
+153
View File
@@ -0,0 +1,153 @@
<?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/>.
/**
* The forum module mail generation tests for groups.
*
* @package mod_forum
* @copyright 2013 Andrew Nicols
* @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(__DIR__ . '/cron_trait.php');
require_once(__DIR__ . '/generator_trait.php');
/**
* The forum module mail generation tests for groups.
*
* @copyright 2013 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_forum_qanda_testcase extends advanced_testcase {
// Make use of the cron tester trait.
use mod_forum_tests_cron_trait;
// Make use of the test generator trait.
use mod_forum_tests_generator_trait;
/**
* @var \phpunit_message_sink
*/
protected $messagesink;
/**
* @var \phpunit_mailer_sink
*/
protected $mailsink;
public function setUp() {
global $CFG;
// 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();
\mod_forum\subscriptions::reset_discussion_cache();
// Messaging is not compatible with transactions...
$this->preventResetByRollback();
// Catch all messages.
$this->messagesink = $this->redirectMessages();
$this->mailsink = $this->redirectEmails();
// Forcibly reduce the maxeditingtime to a second in the past to
// ensure that messages are sent out.
$CFG->maxeditingtime = -1;
}
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();
$this->messagesink->clear();
$this->messagesink->close();
unset($this->messagesink);
$this->mailsink->clear();
$this->mailsink->close();
unset($this->mailsink);
}
/**
* Test that a user who has not posted in a q&a forum does not receive
* notificatinos.
*/
public function test_user_has_not_posted() {
global $CFG, $DB;
$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,
'groupmode' => SEPARATEGROUPS,
'type' => 'qanda',
]);
// Create three students:
// - author, enrolled in group A; and
// - recipient, enrolled in group B; and
// - other, enrolled in the course, but no groups.
list($author, $recipient, $otheruser) = $this->helper_create_users($course, 3);
// Create one editing teacher, not in any group but with accessallgroups capability.
list($editingteacher) = $this->helper_create_users($course, 1, 'editingteacher');
// Post a discussion to the forum.
list($discussion, $post) = $this->helper_post_to_forum($forum, $editingteacher);
$reply = $this->helper_reply_to_post($post, $author);
$otherreply = $this->helper_reply_to_post($post, $recipient);
$DB->execute("UPDATE {forum_posts} SET modified = modified - 1");
$DB->execute("UPDATE {forum_posts} SET created = created - 1");
$DB->execute("UPDATE {forum_discussions} SET timemodified = timemodified - 1");
// Only the author, recipient, and teachers should receive.
$expect = [
'author' => (object) [
'userid' => $author->id,
'messages' => 3,
],
'recipient' => (object) [
'userid' => $recipient->id,
'messages' => 3,
],
'otheruser' => (object) [
'userid' => $otheruser->id,
'messages' => 3,
],
'editingteacher' => (object) [
'userid' => $editingteacher->id,
'messages' => 3,
],
];
$this->queue_tasks_and_assert($expect);
$posts = [$post, $reply, $otherreply];
// No notifications should be queued.
$this->send_notifications_and_assert($author, $posts);
$this->send_notifications_and_assert($recipient, $posts);
$this->send_notifications_and_assert($otheruser, [$post]);
$this->send_notifications_and_assert($editingteacher, $posts);
}
}