diff --git a/classes/manager/mail_manager.php b/classes/manager/mail_manager.php index 1c80659e88..4fe9c43294 100644 --- a/classes/manager/mail_manager.php +++ b/classes/manager/mail_manager.php @@ -27,10 +27,17 @@ use context_course; use context_module; use core\context\course; +use core\cron; +use core\message\message; use core_php_time_limit; +use core_user; +use dml_exception; use mod_moodleoverflow\anonymous; use mod_moodleoverflow\output\moodleoverflow_email; use mod_moodleoverflow\subscriptions; +use moodle_exception; +use moodle_url; +use renderer_base; use stdClass; /** @@ -60,134 +67,345 @@ class mail_manager { const MOODLEOVERFLOW_MAILED_REVIEW_SUCCESS = 3; /** - * Sends mail notifications about new posts. + * This functions executes the task of sending a notification mail to users that are subscribed to a moodleoverflow. + * This function does the following: + * - Retrieve all posts that are unmailed and need to be send. + * * * @return bool + * @throws moodle_exception + * @throws dml_exception */ public static function moodleoverflow_send_mails(): bool { - global $CFG, $DB, $PAGE; - - // Get the course object of the top level site. - $site = get_site(); + global $DB, $PAGE; // Get the main renderers. $htmlout = $PAGE->get_renderer('mod_moodleoverflow', 'email', 'htmlemail'); $textout = $PAGE->get_renderer('mod_moodleoverflow', 'email', 'textemail'); - // Initiate the arrays that are saving the users that are subscribed to posts that needs sending. - $users = []; + // Posts older than x days will not be mailed. + // This will avoid problems with the cron not being run for a long time. + $timenow = time(); + $endtime = $timenow - get_config('moodleoverflow', 'maxeditingtime'); + $starttime = $endtime - (get_config('moodleoverflow', 'maxmailingtime') * 60 * 60); - // Status arrays. - $mailcount = []; - $errorcount = []; + // Retrieve posts that need to be send to users. + if (!$records = self::moodleoverflow_get_unmailed_posts($starttime, $endtime)) { + return true; + } - // Cache arrays. - $discussions = []; - $moodleoverflows = []; + // Mark those posts as mailed. + if (!self::moodleoverflow_mark_old_posts_as_mailed($endtime)) { + return false; + } + + // Start processing the records. + // Build cache arrays for most important objects. All caches are structured with id => object. + $posts = []; + $authors = []; + $recipients = []; $courses = []; + $moodleoverflows = []; + $discussions = []; $coursemodules = []; - // Posts older than x days will not be mailed. This will avoid problems with the cron not ran for a long time. - $timenow = time(); - $endtime = $timenow - get_config('moodleoverflow', 'maxeditingtime'); - $starttime = $endtime - (get_config('moodleoverflow', 'maxmailingtime') * 60 * 60); + // Loop through each records. + foreach ($records as $record) { + // Terminate if the process takes more time then two minutes. + + // Fill the caches with objects if needed. + // Add additional information that were not retrievable from the database to the objects if needed. + self::moodleoverflow_update_mail_caches($record, $coursemodules, $courses, $moodleoverflows, + $discussions, $posts, $authors, $recipients); - // Retrieve all unmailed posts. - $posts = self::moodleoverflow_get_unmailed_posts($starttime, $endtime); - if ($posts) { - // Mark those posts as mailed. - if (!self::moodleoverflow_mark_old_posts_as_mailed($endtime)) { - mtrace('Errors occurred while trying to mark some posts as being mailed.'); - return false; + // Filter records that are not getting mailed. + // Check if the user can see the post. + if (!moodleoverflow_user_can_see_post($moodleoverflows[$record->moodleoverflowid], $discussions[$record->discussionid], + $posts[$record->postid], $coursemodules[$record->cmid])) { + continue; } - // Loop through all posts to be mailed. - foreach ($posts as $postid => $post) { - self::check_post($post, $mailcount, $users, $discussions, $errorcount, $posts, $postid, - $moodleoverflows, $courses, $coursemodules); + + // Check if the user subscribed to the post wants a summary instead of a notification mail. + if ($record->usertomaildigest != 0) { + // Process the record for the mail digest. + self::moodleoverflow_process_maildigest_record($record); + continue; } - } - // Send mails to the users with information about the posts. - if ($users && $posts) { - // Send one mail to every user. - foreach ($users as $userto) { - // Terminate if the process takes more time then two minutes. - core_php_time_limit::raise(120); - - // Tracing information. - mtrace('Processing user ' . $userto->id); - // Initiate the user caches to save memory. - $userto = clone($userto); - $userto->ciewfullnames = []; - $userto->canpost = []; - $userto->markposts = []; - - // Cache the capabilities of the user. - $CFG->branch >= 402 ? \core\cron::setup_user($userto) : cron_setup_user($userto); - - // Reset the caches. - foreach ($coursemodules as $moodleoverflowid) { - $coursemodules[$moodleoverflowid]->cache = new stdClass(); - $coursemodules[$moodleoverflowid]->cache->caps = []; - unset($coursemodules[$moodleoverflowid]->uservisible); - } + // Determine if the author should be anonymous. + $authoranonymous = match ((int)$record->moodleoverflowanonymous) { + anonymous::NOT_ANONYMOUS => false, + anonymous::EVERYTHING_ANONYMOUS => true, + anonymous::QUESTION_ANONYMOUS => ($record->discussionuserid == $record->authorid) + }; + + // Set the userfrom variable, that is anonymous or the post author. + $authoranonymous ? $userfrom = core_user::get_noreply_user() : $userfrom = clone($authors[$record->authorid]); + $userfrom->anonymous = $authoranonymous; + + // Cache the recipients capabilities to view full names for the moodleoverflow instance. + if (!isset($recipients[$record->usertoid]->viewfullnames[$record->moodleoverflowid])) { + // Find the context module. + $modulecontext = context_module::instance($record->cmid); + + // Check the users capabilities. + $recipients[$record->usertoid]->viewfullnames[$record->moodleoverflowid] = + has_capability('moodle/site:viewfullnames', $modulecontext, $record->usertoid); + } - // Loop through all posts of this users. - foreach ($posts as $post) { - self::send_post($userto, $post, $coursemodules, $errorcount, - $discussions, $moodleoverflows, $courses, $mailcount, $users, $site, $textout, $htmlout); - } + // Cache the recipients capability to post in the discussion. + if (!isset($recipients[$record->usertoid]->canpost[$record->discussionid])) { + // Find the context module. + $modulecontext = context_module::instance($record->cmid); - // Release the memory. - unset($userto); + // Check the users capabilities. + $canreply = moodleoverflow_user_can_post($modulecontext, $posts[$record->postid], $record->usertoid); + $recipients[$record->usertoid]->canpost[$record->discussionid] = $canreply; } - } - // Check for all posts whether errors occurred. - foreach ($posts as $post) { - // Tracing information. - mtrace($mailcount[$post->id] . " users were sent post $post->id"); + // Preparation complete. Ready to send message. + + // Build the mail object. + $email = new moodleoverflow_email( + $courses[$record->courseid], + $coursemodules[$record->cmid], + $moodleoverflows[$record->moodleoverflowid], + $discussions[$record->discussionid], + $posts[$record->postid], + $userfrom, + $recipients[$record->usertoid], + $recipients[$record->usertoid]->canpost[$record->discussionid] + ); + + // LEARNWEB-TODO: check if this is needed. + $email->viewfullnames = $recipients[$record->usertoid]->viewfullnames[$record->moodleoverflowid]; + + // The email object is build. Now build all data that is needed for the event that really send the mail. + + // Build post subject. + $subject = html_to_text(get_string('postmailsubject', 'moodleoverflow', + ['subject' => $email->get_subject(), 'courseshortname' => $email->get_coursename()]), 0); + + // Finally: send the notification mail. + $userto = $recipients[$record->usertoid]; + $htmlmessage = $htmlout->render($email); + $textmessage = $textout->render($email); - // Mark the posts with errors in the database. - if ($errorcount[$post->id]) { - $DB->set_field('moodleoverflow_posts', 'mailed', self::MOODLEOVERFLOW_MAILED_ERROR, ['id' => $post->id]); + $mailsent = email_to_user($userto, core_user::get_noreply_user(), $subject, $textmessage, $htmlmessage); + + // Check if an error occurred and mark the post as mailed_error. + if (!$mailsent) { + // A mail does not get resend if it was not sent successfully. + $DB->set_field('moodleoverflow_posts', 'mailed', MOODLEOVERFLOW_MAILED_ERROR, ['id' => $record->postid]); } } - // The task was completed. + // The task is completed. return true; } + /** - * Returns a list of all posts that have not been mailed yet. + * Return a list of records that will be mailed. One record has all the information that is needed. This includes: + * - The post, discussion, moodleoverflow data of a post that is unmailed + * - The data of the post author + * - The data of the user, that is subscribed to the moodleoverflow discussion, that has the unmailed post + * + * The same post and user can be found redundantly, because one posts is mailed to many user and one user gets notified about + * many posts. Because all data is in one table, every record represents one mail. * * @param int $starttime posts created after this time - * @param int $endtime posts created before this time + * @param int $endtime posts created before this time * * @return array + * @throws dml_exception */ - public static function moodleoverflow_get_unmailed_posts($starttime, $endtime) { + public static function moodleoverflow_get_unmailed_posts($starttime, $endtime): array { global $DB; - // Set params for the sql query. - $params = []; - $params['ptimestart'] = $starttime; - $params['ptimeend'] = $endtime; + // Define fields that will be retrieved from the database. + $postfields = "p.id AS postid, p.message AS postmessage, p.messageformat as postmessageformat, p.modified as postmodified, + p.parent AS postparent, p.userid AS postuserid, p.reviewed AS postreviewed"; + $discussionfields = "d.id AS discussionid, d.name AS discussionname, d.userid AS discussionuserid, + d.firstpost AS discussionfirstpost"; + $moodleoverflowfields = "mo.id AS moodleoverflowid, mo.name AS moodleoverflowname, mo.anonymous AS moodleoverflowanonymous, + mo.forcesubscribe AS moodleoverflowforcesubscribe"; + $coursefields = "c.id AS courseid, c.idnumber AS courseidnumber, c.fullname AS coursefullname, + c.shortname AS courseshortname"; + $cmfields = "cm.id AS cmid, cm.groupingid AS cmgroupingid"; + $authorfields = "author.id AS authorid, author.firstname AS authorfirstname, author.lastname AS authorlastname, + author.firstnamephonetic AS authorfirstnamephonetic, author.lastnamephonetic AS authorlastnamephonetic, + author.middlename AS authormiddlename, author.alternatename AS authoralternatename, + author.picture AS authorpicture, author.imagealt AS authorimagealt, author.email AS authoremail"; + $usertofields = "userto.id AS usertoid, userto.maildigest AS usertomaildigest, userto.mailformat AS usertomailformat, + userto.maildisplay AS usertomaildisplay, userto.description AS usertodescription, + userto.password AS usertopassword, userto.lang AS usertolang, userto.auth AS usertoauth, + userto.suspended AS usertosuspended, userto.deleted AS usertodeleted, userto.emailstop AS usertoemailstop, + userto.email AS usertoemail, userto.username AS usertousername, userto.firstname AS usertofirstname, + userto.lastname AS usertolastname, userto.firstnamephonetic AS usertofirstnamephonetic, + userto.lastnamephonetic AS usertolastnamephonetic, userto.middlename AS usertomiddlename, + userto.alternatename AS usertoalternatename"; + + $fields = "(ROW_NUMBER() OVER (ORDER BY p.modified)) AS row_num, " . $postfields . ", " . $discussionfields . ", " + . $moodleoverflowfields . ", " . $coursefields . ", " . $cmfields . ", " . $authorfields . ", " . $usertofields; - $pendingmail = self::MOODLEOVERFLOW_MAILED_PENDING; - $reviewsent = self::MOODLEOVERFLOW_MAILED_REVIEW_SUCCESS; + // Set params for the sql query. + $timenow = round(time(), -2); + $params = [ + 'unsubscribed' => subscriptions::MOODLEOVERFLOW_DISCUSSION_UNSUBSCRIBED, + 'forcesubscribe' => MOODLEOVERFLOW_FORCESUBSCRIBE, + 'active' => ENROL_USER_ACTIVE, + 'enabled' => ENROL_INSTANCE_ENABLED, + 'now1' => $timenow, + 'now2' => $timenow, + 'pendingmail' => self::MOODLEOVERFLOW_MAILED_PENDING, + 'reviewsent' => self::MOODLEOVERFLOW_MAILED_REVIEW_SUCCESS, + 'ptimestart' => $starttime, + 'ptimeend' => $endtime, + ]; // Retrieve the records. - $sql = "SELECT p.*, d.course, d.moodleoverflow - FROM {moodleoverflow_posts} p - JOIN {moodleoverflow_discussions} d ON d.id = p.discussion - WHERE p.mailed IN ($pendingmail, $reviewsent) AND p.reviewed = 1 - AND COALESCE(p.timereviewed, p.created) >= :ptimestart AND p.created < :ptimeend - ORDER BY p.modified ASC"; + // Documentation can be found on: https://github.com/learnweb/moodle-mod_moodleoverflow/wiki/Documentation-for-Developers. + $sql = "SELECT $fields + FROM {moodleoverflow_posts} p + JOIN {moodleoverflow_discussions} d ON d.id = p.discussion + JOIN {moodleoverflow} mo ON mo.id = d.moodleoverflow + JOIN {course} c ON c.id = mo.course + JOIN ( + SELECT cm.id, cm.groupingid, cm.instance + FROM {course_modules} cm + JOIN {modules} md ON md.id = cm.module + WHERE md.name = 'moodleoverflow' + ) cm ON cm.instance = mo.id + JOIN {user} author ON author.id = p.userid + JOIN ( + SELECT * + FROM ( + SELECT userid, moodleoverflow, -1 as discussion + FROM {moodleoverflow_subscriptions} s + UNION + SELECT userid, moodleoverflow, discussion + FROM {moodleoverflow_discuss_subs} ds + WHERE ds.preference <> :unsubscribed + UNION + SELECT userid, moodleoverflow, discussion + FROM ( + SELECT u.id AS userid, m.id AS moodleoverflow, -1 AS discussion + FROM {user} u + JOIN {user_enrolments} ue ON ue.userid = u.id + JOIN {enrol} e ON e.id = ue.enrolid + JOIN {course} c ON c.id = e.courseid + JOIN {moodleoverflow} m ON m.course = c.id + WHERE m.forcesubscribe = :forcesubscribe + AND ue.status = :active + AND e.status = :enabled + AND ue.timestart < :now1 + AND (ue.timeend = 0 OR ue.timeend > :now2) + ) as forcedsubs + ) as subscriptions + LEFT JOIN {user} u ON u.id = subscriptions.userid + ORDER BY u.email ASC + ) userto ON ((d.moodleoverflow = userto.moodleoverflow) AND + ((p.discussion = userto.discussion) OR + (userto.discussion = -1)) + ) + WHERE p.mailed IN (:pendingmail, :reviewsent) AND p.reviewed = 1 + AND COALESCE(p.timereviewed, p.created) >= :ptimestart AND p.created < :ptimeend + AND author.id <> userto.id"; return $DB->get_records_sql($sql, $params); } + /** + * Fills and updates cache arrays with data from a record object. + * This function checks if specific data (course modules, courses, moodleoverflows, discussions, posts, authors, recipients) + * is already cached. If not, it creates an object with the relevant data from the provided record and stores it in the cache. + * + * @param object $record The record containing data to be cached. + * @param array $coursemodules Cache for course module data, indexed by course module ID. + * @param array $courses Cache for course data, indexed by course ID. + * @param array $moodleoverflows Cache for moodleoverflow data, indexed by moodleoverflow ID. + * @param array $discussions Cache for discussion data, indexed by discussion ID. + * @param array $posts Cache for post data, indexed by post ID. + * @param array $authors Cache for author data, indexed by author ID. + * @param array $recipients Cache for recipient data, indexed by recipient ID. + * + * @return void + */ + public static function moodleoverflow_update_mail_caches(object $record, array &$coursemodules, array &$courses, + array &$moodleoverflows, array &$discussions, array &$posts, + array &$authors, array &$recipients ): void { + // Define cache types and their corresponding record properties. + $cachetypes = [ + 'coursemodules' => ['id' => 'cmid', 'groupingid' => 'cmgroupingid'], + 'courses' => ['id' => 'courseid', 'idnumber' => 'courseidnumber', 'fullname' => 'coursefullname', + 'shortname' => 'courseshortname'], + 'moodleoverflows' => ['id' => 'moodleoverflowid', 'name' => 'moodleoverflowname', + 'anonymous' => 'moodleoverflowanonymous', 'forcesubscribe' => 'moodleoverflowforcesubscribe'], + 'discussions' => ['id' => 'discussionid', 'name' => 'discussionname', 'userid' => 'discussionuserid', + 'firstpost' => 'discussionfirstpost'], + 'posts' => ['id' => 'postid', 'message' => 'postmessage', 'messageformat' => 'postmessageformat', + 'modified' => 'postmodified', 'parent' => 'postparent', 'userid' => 'postuserid', + 'reviewed' => 'postreviewed'], + 'authors' => ['id' => 'authorid', 'firstname' => 'authorfirstname', 'lastname' => 'authorlastname', + 'firstnamephonetic' => 'authorfirstnamephonetic', 'lastnamephonetic' => 'authorlastnamephonetic', + 'middlename' => 'authormiddlename', 'alternatename' => 'authoralternatename', + 'picture' => 'authorpicture', 'imagealt' => 'authorimagealt', 'email' => 'authoremail'], + 'recipients' => ['id' => 'usertoid', 'maildigest' => 'usertomaildigest', 'mailformat' => 'usertomailformat', + 'maildisplay' => 'usertomaildisplay', 'description' => 'usertodescription', + 'password' => 'usertopassword', 'lang' => 'usertolang', 'auth' => 'usertoauth', + 'suspended' => 'usertosuspended', 'deleted' => 'usertodeleted', + 'emailstop' => 'usertoemailstop', 'email' => 'usertoemail', 'username' => 'usertousername', + 'firstname' => 'usertofirstname', 'lastname' => 'usertolastname', + 'firstnamephonetic' => 'usertofirstnamephonetic', 'lastnamephonetic' => 'usertolastnamephonetic', + 'middlename' => 'usertomiddlename', 'alternatename' => 'usertoalternatename'], + ]; + + // Iterate over cache types and update caches if not already set. + foreach ($cachetypes as $cachename => $properties) { + $cachekey = $record->{$properties['id']}; + if (!isset(${$cachename}[$cachekey])) { + $obj = new stdClass(); + foreach ($properties as $propname => $recordkey) { + $obj->$propname = $record->$recordkey; + } + // Only for recipients, add empty arrays for viewfullnames and canpost. + if ($cachename === 'recipients') { + $obj->viewfullnames = []; + $obj->canpost = []; + } + ${$cachename}[$cachekey] = $obj; + } + } + } + + /** + * Function that processes a record from self::moodleoverflow_get_unmailed_posts() if the user that gets the mail wants a + * resume instead of a mail for every post. + * + * @param object $data a single record object from self::moodleoverflow_get_unmailed_posts() + * @return void + */ + public static function moodleoverflow_process_maildigest_record(object $data): void { + global $DB; + // LEARNWEB-TODO: Rename database table attribute names. Rethink the table structure. What should the mail have? + // If the record exists, update it. If not, insert a new record. + if ($dbrecord = $DB->get_record('moodleoverflow_mail_info', ['userid' => $data->usertoid, 'courseid' => $data->courseid, + 'forumid' => $data->moodleoverflowid, 'forumdiscussionid' => $data->discussionid, ], 'numberofposts, id')) { + $dbrecord->numberofposts++; + $DB->update_record('moodleoverflow_mail_info', $dbrecord); + } else { + $record = (object) [ + 'userid' => $data->usertoid, + 'courseid' => $data->courseid, + 'forumid' => $data->moodleoverflowid, + 'forumdiscussionid' => $data->discussionid, + 'numberofposts' => 1, + ]; + $DB->insert_record('moodleoverflow_mail_info', $record); + } + } + /** * Marks posts before a certain time as being mailed already. * @@ -211,391 +429,10 @@ public static function moodleoverflow_mark_old_posts_as_mailed($endtime) { // Define the sql query. $sql = "UPDATE {moodleoverflow_posts} - SET mailed = :mailedsuccess - WHERE (created < :endtime) AND mailed IN (:mailedpending, :mailedreviewsent) AND reviewed = 1"; + SET mailed = :mailedsuccess + WHERE (created < :endtime) AND mailed IN (:mailedpending, :mailedreviewsent) AND reviewed = 1"; return $DB->execute($sql, $params); } - /** - * Removes unnecessary information from the user records for the mail generation. - * - * @param stdClass $user - */ - public static function moodleoverflow_minimise_user_record(stdClass $user) { - // Remove all information for the mail generation that are not needed. - unset($user->institution); - unset($user->department); - unset($user->address); - unset($user->city); - unset($user->url); - unset($user->currentlogin); - unset($user->description); - unset($user->descriptionformat); - } - - /** - * Check for a single post if the mail should be send. This includes: - * 1) Does a) the moodleoverflow - * b) moodleoverflow discussion - * c) course module - * still exists? - * 2) Is the user subscriped? - * @param stdClass $post - * @param array $mailcount - * @param array $users - * @param array $discussions - * @param array $errorcount - * @param array $posts - * @param int $postid - * @param array $moodleoverflows - * @param array $courses - * @param array $coursemodules - * @return void - * @throws \coding_exception - * @throws \dml_exception - */ - private static function check_post($post, array &$mailcount, array &$users, array &$discussions, array &$errorcount, - array &$posts, int $postid, array &$moodleoverflows, array &$courses, - array &$coursemodules) { - // Check the cache if the discussion exists. - $discussionid = $post->discussion; - if (!self::cache_record('moodleoverflow_discussions', $discussionid, $discussions, - 'Could not find discussion ', $posts, $postid, true)) { - return; - } - - // Retrieve the connected moodleoverflow instance from the database. - $moodleoverflowid = $discussions[$discussionid]->moodleoverflow; - if (!self::cache_record('moodleoverflow', $moodleoverflowid, $moodleoverflows, - 'Could not find moodleoverflow ', $posts, $postid, false)) { - return; - } - - // Retrieve the connected courses from the database. - $courseid = $moodleoverflows[$moodleoverflowid]->course; - if (!self::cache_record('course', $courseid, $courses, - 'Could not find course ', $posts, $postid, false)) { - return; - } - - // Retrieve the connected course modules from the database. - if (!isset($coursemodules[$moodleoverflowid])) { - // Retrieve the coursemodule and update the cache. - if ($cm = get_coursemodule_from_instance('moodleoverflow', $moodleoverflowid, $courseid)) { - $coursemodules[$moodleoverflowid] = $cm; - } else { - mtrace('Could not find course module for moodleoverflow ' . $moodleoverflowid); - unset($posts[$postid]); - return; - } - } - - // Cache subscribed users of each moodleoverflow. - if (!isset($subscribedusers[$moodleoverflowid])) { - // Retrieve the context module. - $modulecontext = context_module::instance($coursemodules[$moodleoverflowid]->id); - - // Retrieve all subscribed users. - $mid = $moodleoverflows[$moodleoverflowid]; - if ($subusers = subscriptions::get_subscribed_users($mid, $modulecontext, 'u.*', true)) { - // Loop through all subscribed users. - foreach ($subusers as $postuser) { - // Save the user into the cache. - $subscribedusers[$moodleoverflowid][$postuser->id] = $postuser->id; - self::moodleoverflow_minimise_user_record($postuser); - self::moodleoverflow_minimise_user_record($postuser); - $users[$postuser->id] = $postuser; - } - - // Release the memory. - unset($subusers); - unset($postuser); - } - } - - // Initiate the count of the mails send and errors. - $mailcount[$postid] = 0; - $errorcount[$postid] = 0; - } - - /** - * Helper function for check_post(). Caches the a record exists in the database and caches the record if needed. - * @param string $table - * @param int $id - * @param array $cache - * @param string $errormessage - * @param array $posts - * @param int $postid - * @param bool $fillsubscache If the subscription cache is being filled (only when checking discussion cache) - * @return bool - * @throws \dml_exception - */ - private static function cache_record($table, $id, &$cache, $errormessage, &$posts, $postid, $fillsubscache) { - global $DB; - // Check if cache if an record exists already in the cache. - if (!isset($cache[$id])) { - // If there is a record in the database, update the cache. Else ignore the post. - if ($record = $DB->get_record($table, ['id' => $id])) { - $cache[$id] = $record; - if ($fillsubscache) { - subscriptions::fill_subscription_cache($record->moodleoverflow); - subscriptions::fill_discussion_subscription_cache($record->moodleoverflow); - } - } else { - mtrace($errormessage . $id); - unset($posts[$postid]); - return false; - } - } - return true; - } - - - /** - * Send the Mail with information of the post depending on theinformation available. - * E.g. anonymous post do not include names, users who want resumes do not get single mails. - * @param stdClass $userto - * @param stdClass $post - * @param array $coursemodules - * @param array $errorcount - * @param array $discussions - * @param array $moodleoverflows - * @param array $courses - * @param array $mailcount - * @param array $users - * @param stdClass $site - * @param stdClass $textout - * @param stdClass $htmlout - * @return void - * @throws \coding_exception - * @throws \dml_exception - * @throws \moodle_exception - */ - private static function send_post($userto, $post, array &$coursemodules, array &$errorcount, - array &$discussions, array &$moodleoverflows, array &$courses, array &$mailcount, - array &$users, $site, $textout, $htmlout) { - global $DB, $CFG; - - // Initiate variables for the post. - $discussion = $discussions[$post->discussion]; - $moodleoverflow = $moodleoverflows[$discussion->moodleoverflow]; - $course = $courses[$moodleoverflow->course]; - $cm =& $coursemodules[$moodleoverflow->id]; - $modulecontext = context_module::instance($cm->id); - - // Check if user wants a resume. - // in this case: make a new dataset in "moodleoverflow_mail_info" to save the posts data. - // Dataset from moodleoverflow_mail_info will be send later in a mail. - $usermailsetting = $userto->maildigest; - if ($usermailsetting != 0) { - $dataobject = new stdClass(); - $dataobject->userid = $userto->id; - $dataobject->courseid = $course->id; - $dataobject->forumid = $moodleoverflow->id; - $dataobject->forumdiscussionid = $discussion->id; - $record = $DB->get_record('moodleoverflow_mail_info', - ['userid' => $dataobject->userid, - 'courseid' => $dataobject->courseid, - 'forumid' => $dataobject->forumid, - 'forumdiscussionid' => $dataobject->forumdiscussionid, ], - 'numberofposts, id'); - if (is_object($record)) { - $dataset = $record; - $dataobject->numberofposts = $dataset->numberofposts + 1; - $dataobject->id = $dataset->id; - $DB->update_record('moodleoverflow_mail_info', $dataobject); - } else { - $dataobject->numberofposts = 1; - $DB->insert_record('moodleoverflow_mail_info', $dataobject); - } - return; - } - - // Check whether the user is subscribed. - if (!isset($subscribedusers[$moodleoverflow->id][$userto->id])) { - return; - } - - // Check whether the user is subscribed to the discussion. - $uid = $userto->id; - if (!subscriptions::is_subscribed($uid, $moodleoverflow, $modulecontext, $post->discussion)) { - return; - } - - // Check whether the user unsubscribed to the discussion after it was created. - $subnow = subscriptions::fetch_discussion_subscription($moodleoverflow->id, $userto->id); - if ($subnow && isset($subnow[$post->discussion]) && ($subnow[$post->discussion] > $post->created)) { - return; - } - - if (anonymous::is_post_anonymous($discussion, $moodleoverflow, $post->userid)) { - $userfrom = \core_user::get_noreply_user(); - } else { - // Check whether the sending user is cached already. - if (array_key_exists($post->userid, $users)) { - $userfrom = $users[$post->userid]; - } else { - // We dont know the the user yet. - - // Retrieve the user from the database. - $userfrom = $DB->get_record('user', ['id' => $post->userid]); - if ($userfrom) { - self::moodleoverflow_minimise_user_record($userfrom); - } else { - $uid = $post->userid; - $pid = $post->id; - mtrace('Could not find user ' . $uid . ', author of post ' . $pid . '. Unable to send message.'); - return; - } - } - } - - // Setup roles and languages. - $CFG->branch >= 402 ? \core\cron::setup_user($userto, $course) : cron_setup_user($userto, $course); - - // Cache the users capability to view full names. - if (!isset($userto->viewfullnames[$moodleoverflow->id])) { - - // Find the context module. - $modulecontext = context_module::instance($cm->id); - - // Check the users capabilities. - $userto->viewfullnames[$moodleoverflow->id] = has_capability('moodle/site:viewfullnames', $modulecontext); - } - - // Cache the users capability to post in the discussion. - if (!isset($userto->canpost[$discussion->id])) { - - // Find the context module. - $modulecontext = context_module::instance($cm->id); - - // Check the users capabilities. - $canpost = moodleoverflow_user_can_post($modulecontext, $post, $userto->id); - $userto->canpost[$discussion->id] = $canpost; - } - - // Make sure the current user is allowed to see the post. - if (!moodleoverflow_user_can_see_post($moodleoverflow, $discussion, $post, $cm)) { - mtrace('User ' . $userto->id . ' can not see ' . $post->id . '. Not sending message.'); - return; - } - - // Sent the email. - - // Preapare to actually send the post now. Build up the content. - $cleanname = str_replace('"', "'", strip_tags(format_string($moodleoverflow->name))); - $shortname = format_string($course->shortname, true, ['context' => context_course::instance($course->id)]); - - // Define a header to make mails easier to track. - $emailmessageid = generate_email_messageid('moodlemoodleoverflow' . $moodleoverflow->id); - $userfrom->customheaders = [ - 'List-Id: "' . $cleanname . '" ' . $emailmessageid, - 'List-Help: ' . $CFG->wwwroot . '/mod/moodleoverflow/view.php?m=' . $moodleoverflow->id, - 'Message-ID: ' . generate_email_messageid(hash('sha256', $post->id . 'to' . $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', - ]; - - // Cache the users capabilities. - if (!isset($userto->canpost[$discussion->id])) { - $canreply = moodleoverflow_user_can_post($modulecontext, $post, $userto->id); - } else { - $canreply = $userto->canpost[$discussion->id]; - } - - // Format the data. - $data = new moodleoverflow_email($course, $cm, $moodleoverflow, $discussion, $post, $userfrom, $userto, $canreply); - - // Retrieve the unsubscribe-link. - $userfrom->customheaders[] = sprintf('List-Unsubscribe: <%s>', $data->get_unsubscribediscussionlink()); - - // Check the capabilities to view full names. - if (!isset($userto->viewfullnames[$moodleoverflow->id])) { - $data->viewfullnames = has_capability('moodle/site:viewfullnames', $modulecontext, $userto->id); - } else { - $data->viewfullnames = $userto->viewfullnames[$moodleoverflow->id]; - } - - // Retrieve needed variables for the mail. - $var = new stdClass(); - $var->subject = $data->get_subject(); - $var->moodleoverflowname = $cleanname; - $var->sitefullname = format_string($site->fullname); - $var->siteshortname = format_string($site->shortname); - $var->courseidnumber = $data->get_courseidnumber(); - $var->coursefullname = $data->get_coursefullname(); - $var->courseshortname = $data->get_coursename(); - $postsubject = html_to_text(get_string('postmailsubject', 'moodleoverflow', $var), 0); - $rootid = generate_email_messageid(hash('sha256', $discussion->firstpost . 'to' . $userto->id)); - - // Check whether the post is a reply. - if ($post->parent) { - // Add a reply header. - $parentid = generate_email_messageid(hash('sha256', $post->parent . 'to' . $userto->id)); - $userfrom->customheaders[] = "In-Reply-To: $parentid"; - - // Comments need a reference to the starting post as well. - if ($post->parent != $discussion->firstpost) { - $userfrom->customheaders[] = "References: $rootid $parentid"; - } else { - $userfrom->customheaders[] = "References: $parentid"; - } - } - - // Send the post now. - mtrace('Sending ', ''); - - // Create the message event. - $eventdata = new \core\message\message(); - $eventdata->courseid = $course->id; - $eventdata->component = 'mod_moodleoverflow'; - $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; - - // Initiate another message array. - $small = new stdClass(); - $small->user = fullname($userfrom); - $formatedstring = format_string($moodleoverflow->name, true); - $small->moodleoverflowname = "$shortname: " . $formatedstring . ": " . $discussion->name; - $small->message = $post->message; - - // Make sure the language is correct. - $usertol = $userto->lang; - $eventdata->smallmessage = get_string_manager()->get_string('smallmessage', 'moodleoverflow', $small, $usertol); - - // Generate the url to view the post. - $url = '/mod/moodleoverflow/discussion.php'; - $params = ['d' => $discussion->id]; - $contexturl = new moodle_url($url, $params, 'p' . $post->id); - $eventdata->contexturl = $contexturl->out(); - $eventdata->contexturlname = $discussion->name; - - // Actually send the message. - $mailsent = message_send($eventdata); - - // Check whether the sending failed. - if (!$mailsent) { - mtrace('Error: mod/moodleoverflow/classes/task/send_mail.php execute(): ' . - "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]++; - } - - // Tracing message. - mtrace('post ' . $post->id . ': ' . $discussion->name); - } - } diff --git a/classes/output/moodleoverflow_email.php b/classes/output/moodleoverflow_email.php index 27865bb49c..f5c57f7d54 100644 --- a/classes/output/moodleoverflow_email.php +++ b/classes/output/moodleoverflow_email.php @@ -25,6 +25,7 @@ namespace mod_moodleoverflow\output; use mod_moodleoverflow\anonymous; +use mod_moodleoverflow\subscriptions; /** * Moodleoverflow email renderable for use in e-mail. @@ -251,8 +252,7 @@ public function __set($name, $value) { public function get_unsubscribediscussionlink() { // Check whether the moodleoverflow is subscribable. - $subscribable = \mod_moodleoverflow\subscriptions::is_subscribable($this->moodleoverflow, - \context_module::instance($this->cm->id)); + $subscribable = subscriptions::is_subscribable($this->moodleoverflow, \context_module::instance($this->cm->id)); if (!$subscribable) { return null; } @@ -432,7 +432,7 @@ public function get_replylink() { * @return string */ public function get_unsubscribemoodleoverflowlink() { - if (!\mod_moodleoverflow\subscriptions::is_subscribable($this->moodleoverflow, + if (!subscriptions::is_subscribable($this->moodleoverflow, \context_module::instance($this->cm->id))) { return null; } diff --git a/classes/subscriptions.php b/classes/subscriptions.php index 14fe64ceb7..e1c0075a22 100644 --- a/classes/subscriptions.php +++ b/classes/subscriptions.php @@ -536,11 +536,8 @@ public static function get_subscribed_users($moodleoverflow, $context, $fields = // Default fields if none are submitted. if (empty($fields)) { - if ($CFG->branch >= 311) { - $allnames = \core_user\fields::for_name()->get_sql('u', false, '', '', false)->selects; - } else { - $allnames = get_all_user_name_fields(true, 'u'); - } + $allnames = \core_user\fields::for_name()->get_sql('u', false, '', '', false)->selects; + $fields = "u.id, u.username, $allnames, u.maildisplay, u.mailformat, u.maildigest, u.imagealt, u.email, u.emailstop, u.city, u.country, u.lastaccess, u.lastlogin, u.picture, u.timezone, u.theme, u.lang, u.trackforums, u.mnethostid"; diff --git a/classes/task/send_daily_mail.php b/classes/task/send_daily_mails.php similarity index 95% rename from classes/task/send_daily_mail.php rename to classes/task/send_daily_mails.php index a4494bea74..807103c140 100644 --- a/classes/task/send_daily_mail.php +++ b/classes/task/send_daily_mails.php @@ -26,7 +26,7 @@ /** * This task sends a daily mail of unread posts */ -class send_daily_mail extends \core\task\scheduled_task { +class send_daily_mails extends \core\task\scheduled_task { /** * Return the task's name as shown in admin screens. @@ -34,7 +34,7 @@ class send_daily_mail extends \core\task\scheduled_task { * @return string */ public function get_name() { - return get_string('tasksenddailymail', 'mod_moodleoverflow'); + return get_string('tasksenddailymails', 'mod_moodleoverflow'); } /** @@ -88,7 +88,7 @@ public function execute() { $message = implode('
', $mail); $userto = $DB->get_record('user', ['id' => $user->userid]); $from = \core_user::get_noreply_user(); - $subject = get_string('tasksenddailymail', 'mod_moodleoverflow'); + $subject = get_string('tasksenddailymails', 'mod_moodleoverflow'); email_to_user($userto, $from, $subject, $message); $DB->delete_records('moodleoverflow_mail_info', ['userid' => $user->userid]); } diff --git a/classes/task/send_mails.php b/classes/task/send_mails.php index d39286e0b6..8fd15f98b7 100644 --- a/classes/task/send_mails.php +++ b/classes/task/send_mails.php @@ -18,151 +18,49 @@ * A scheduled task for moodleoverflow cron. * * @package mod_moodleoverflow - * @copyright 2017 Kennet Winter + * @copyright 2025 Tamaro Walter * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_moodleoverflow\task; -use core\session\exception; -use mod_moodleoverflow\anonymous; -use mod_moodleoverflow\output\moodleoverflow_email; - -defined('MOODLE_INTERNAL') || die(); -require_once(__DIR__ . '/../../locallib.php'); +use coding_exception; +use core\notification; +use Exception; +use lang_string; +use mod_moodleoverflow\manager\mail_manager; /** - * Class for sending mails to users who have subscribed a moodleoverflow. + * Class for sending mails to users that need to review a moodleoverflow post. * * @package mod_moodleoverflow - * @copyright 2017 Kennet Winter + * @copyright 2025 Tamaro Walter * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class send_mails extends \core\task\scheduled_task { /** - * Get a descriptive name for this task (shown to admins). + * Get a descriptive name for this task (shwon to admins). * - * @return string + * @return lang_string|string + * @throws coding_exception */ - public function get_name() { + public function get_name(): lang_string|string { return get_string('tasksendmails', 'mod_moodleoverflow'); } /** * Runs moodleoverflow cron. + * + * @return bool */ - public function execute() { - - // Send mail notifications. - moodleoverflow_send_mails(); - - $this->send_review_notifications(); - - // The cron is finished. - return true; - - } - - /** - * Sends initial notifications for needed reviews to all users with review capability. - */ - public function send_review_notifications() { - global $DB, $OUTPUT, $PAGE, $CFG; - - $rendererhtml = $PAGE->get_renderer('mod_moodleoverflow', 'email', 'htmlemail'); - $renderertext = $PAGE->get_renderer('mod_moodleoverflow', 'email', 'textemail'); - - $postinfos = $DB->get_records_sql( - 'SELECT p.*, d.course as cid, d.moodleoverflow as mid, d.id as did FROM {moodleoverflow_posts} p ' . - 'JOIN {moodleoverflow_discussions} d ON p.discussion = d.id ' . - "WHERE p.mailed = :mailpending AND p.reviewed = 0 AND p.created < :timecutoff " . - "ORDER BY d.course, d.moodleoverflow, d.id", - [ - 'mailpending' => MOODLEOVERFLOW_MAILED_PENDING, - 'timecutoff' => time() - get_config('moodleoverflow', 'reviewpossibleaftertime'), - ] - ); - - if (empty($postinfos)) { - mtrace('No review notifications to send.'); - return; - } - - $course = null; - $moodleoverflow = null; - $usersto = null; - $cm = null; - $discussion = null; - $success = []; - - foreach ($postinfos as $postinfo) { - if ($course == null || $course->id != $postinfo->cid) { - $course = get_course($postinfo->cid); - } - - if ($moodleoverflow == null || $moodleoverflow->id != $postinfo->mid) { - $cm = get_coursemodule_from_instance('moodleoverflow', $postinfo->mid, 0, false, MUST_EXIST); - $modulecontext = \context_module::instance($cm->id); - $userswithcapability = get_users_by_capability($modulecontext, 'mod/moodleoverflow:reviewpost'); - $coursecontext = \context_course::instance($course->id); - $usersenrolled = get_enrolled_users($coursecontext); - $usersto = []; - foreach ($userswithcapability as $user) { - if (in_array($user, $usersenrolled)) { - array_push($usersto, $user); - } - } - - $moodleoverflow = $DB->get_record('moodleoverflow', ['id' => $postinfo->mid], '*', MUST_EXIST); - } - - if ($discussion == null || $discussion->id != $postinfo->did) { - $discussion = $DB->get_record('moodleoverflow_discussions', ['id' => $postinfo->did], '*', MUST_EXIST); - } - - $post = $postinfo; - $userfrom = \core_user::get_user($postinfo->userid, '*', MUST_EXIST); - $userfrom->anonymous = anonymous::is_post_anonymous($discussion, $moodleoverflow, $postinfo->userid); - - foreach ($usersto as $userto) { - try { - // Check for moodle version. Version 401 supported until 8 December 2025. - if ($CFG->branch >= 402) { - \core\cron::setup_user($userto, $course); - } else { - cron_setup_user($userto, $course); - } - - $maildata = new moodleoverflow_email($course, $cm, $moodleoverflow, $discussion, - $post, $userfrom, $userto, false); - - $textcontext = $maildata->export_for_template($renderertext, true); - $htmlcontext = $maildata->export_for_template($rendererhtml, false); - - email_to_user( - $userto, - \core_user::get_noreply_user(), - get_string('email_review_needed_subject', 'moodleoverflow', $textcontext), - $OUTPUT->render_from_template('mod_moodleoverflow/email_review_needed_text', $textcontext), - $OUTPUT->render_from_template('mod_moodleoverflow/email_review_needed_html', $htmlcontext) - ); - } catch (exception $e) { - mtrace("Error sending review notification for post $post->id to user $userto->id!"); - } - } - $success[] = $post->id; - } - - if (!empty($success)) { - list($insql, $inparams) = $DB->get_in_or_equal($success); - $DB->set_field_select( - 'moodleoverflow_posts', 'mailed', MOODLEOVERFLOW_MAILED_REVIEW_SUCCESS, - 'id ' . $insql, $inparams - ); - mtrace('Sent review notifications for ' . count($success) . ' posts successfully!'); + public function execute(): bool { + try { + mail_manager::moodleoverflow_send_mails(); + } catch (Exception $e) { + notification::error(get_string('error_sending_mails', 'mod_moodleoverflow', $e->getMessage())); + return false; } + return true; } - } - diff --git a/classes/task/send_review_mails.php b/classes/task/send_review_mails.php new file mode 100644 index 0000000000..2466c119d9 --- /dev/null +++ b/classes/task/send_review_mails.php @@ -0,0 +1,166 @@ +. + +/** + * A scheduled task for moodleoverflow cron. + * + * @package mod_moodleoverflow + * @copyright 2017 Kennet Winter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_moodleoverflow\task; + +use core\session\exception; +use mod_moodleoverflow\anonymous; +use mod_moodleoverflow\manager\mail_manager; +use mod_moodleoverflow\output\moodleoverflow_email; + +defined('MOODLE_INTERNAL') || die(); +require_once(__DIR__ . '/../../locallib.php'); + +/** + * Class for sending mails to users that need to review a moodleoverflow post. + * + * @package mod_moodleoverflow + * @copyright 2017 Kennet Winter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class send_review_mails extends \core\task\scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('tasksendreviewmails', 'mod_moodleoverflow'); + } + + /** + * Runs moodleoverflow cron. + */ + public function execute() { + // Send review mails. + $this->send_review_notifications(); + + // The cron is finished. + return true; + + } + + /** + * Sends initial notifications for needed reviews to all users with review capability. + */ + public function send_review_notifications() { + global $DB, $OUTPUT, $PAGE, $CFG; + + $rendererhtml = $PAGE->get_renderer('mod_moodleoverflow', 'email', 'htmlemail'); + $renderertext = $PAGE->get_renderer('mod_moodleoverflow', 'email', 'textemail'); + + $postinfos = $DB->get_records_sql( + 'SELECT p.*, d.course as cid, d.moodleoverflow as mid, d.id as did FROM {moodleoverflow_posts} p ' . + 'JOIN {moodleoverflow_discussions} d ON p.discussion = d.id ' . + "WHERE p.mailed = :mailpending AND p.reviewed = 0 AND p.created < :timecutoff " . + "ORDER BY d.course, d.moodleoverflow, d.id", + [ + 'mailpending' => MOODLEOVERFLOW_MAILED_PENDING, + 'timecutoff' => time() - get_config('moodleoverflow', 'reviewpossibleaftertime'), + ] + ); + + if (empty($postinfos)) { + mtrace('No review notifications to send.'); + return; + } + + $course = null; + $moodleoverflow = null; + $usersto = null; + $cm = null; + $discussion = null; + $success = []; + + foreach ($postinfos as $postinfo) { + if ($course == null || $course->id != $postinfo->cid) { + $course = get_course($postinfo->cid); + } + + if ($moodleoverflow == null || $moodleoverflow->id != $postinfo->mid) { + $cm = get_coursemodule_from_instance('moodleoverflow', $postinfo->mid, 0, false, MUST_EXIST); + $modulecontext = \context_module::instance($cm->id); + $userswithcapability = get_users_by_capability($modulecontext, 'mod/moodleoverflow:reviewpost'); + $coursecontext = \context_course::instance($course->id); + $usersenrolled = get_enrolled_users($coursecontext); + $usersto = []; + foreach ($userswithcapability as $user) { + if (in_array($user, $usersenrolled)) { + array_push($usersto, $user); + } + } + + $moodleoverflow = $DB->get_record('moodleoverflow', ['id' => $postinfo->mid], '*', MUST_EXIST); + } + + if ($discussion == null || $discussion->id != $postinfo->did) { + $discussion = $DB->get_record('moodleoverflow_discussions', ['id' => $postinfo->did], '*', MUST_EXIST); + } + + $post = $postinfo; + $userfrom = \core_user::get_user($postinfo->userid, '*', MUST_EXIST); + $userfrom->anonymous = anonymous::is_post_anonymous($discussion, $moodleoverflow, $postinfo->userid); + + foreach ($usersto as $userto) { + try { + // Check for moodle version. Version 401 supported until 8 December 2025. + if ($CFG->branch >= 402) { + \core\cron::setup_user($userto, $course); + } else { + cron_setup_user($userto, $course); + } + + $maildata = new moodleoverflow_email($course, $cm, $moodleoverflow, $discussion, + $post, $userfrom, $userto, false); + + $textcontext = $maildata->export_for_template($renderertext, true); + $htmlcontext = $maildata->export_for_template($rendererhtml, false); + + email_to_user( + $userto, + \core_user::get_noreply_user(), + get_string('email_review_needed_subject', 'moodleoverflow', $textcontext), + $OUTPUT->render_from_template('mod_moodleoverflow/email_review_needed_text', $textcontext), + $OUTPUT->render_from_template('mod_moodleoverflow/email_review_needed_html', $htmlcontext) + ); + } catch (exception $e) { + mtrace("Error sending review notification for post $post->id to user $userto->id!"); + } + } + $success[] = $post->id; + } + + if (!empty($success)) { + list($insql, $inparams) = $DB->get_in_or_equal($success); + $DB->set_field_select( + 'moodleoverflow_posts', 'mailed', MOODLEOVERFLOW_MAILED_REVIEW_SUCCESS, + 'id ' . $insql, $inparams + ); + mtrace('Sent review notifications for ' . count($success) . ' posts successfully!'); + } + } + +} + diff --git a/db/tasks.php b/db/tasks.php index 0a688b84f5..750dd1bc41 100644 --- a/db/tasks.php +++ b/db/tasks.php @@ -26,9 +26,9 @@ $tasks = [ - // Deliver mail notification about new posts. + // Deliver mail notification about posts that need to be reviewed. [ - 'classname' => 'mod_moodleoverflow\task\send_mails', + 'classname' => 'mod_moodleoverflow\task\send_review_mails', 'blocking' => 0, 'minute' => '*', 'hour' => '*', @@ -48,9 +48,9 @@ 'dayofweek' => '*', ], - // Clean old read records. + // Send daily digest mail of unread posts. [ - 'classname' => 'mod_moodleoverflow\task\send_daily_mail', + 'classname' => 'mod_moodleoverflow\task\send_daily_mails', 'blocking' => 0, 'minute' => '0', 'hour' => '17', @@ -58,4 +58,15 @@ 'month' => '*', 'dayofweek' => '*', ], + + // Task to send mail notification of new posts in subscribed discussions. + [ + 'classname' => 'mod_moodleoverflow\task\send_mails', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'month' => '*', + 'dayofweek' => '*', + ], ]; diff --git a/lang/en/moodleoverflow.php b/lang/en/moodleoverflow.php index 169b594b81..f0b433d1f3 100644 --- a/lang/en/moodleoverflow.php +++ b/lang/en/moodleoverflow.php @@ -384,8 +384,9 @@ $string['switchtoauto'] = 'If you switch to the auto subscription, all enrolled users will be subscribed to this forum!'; $string['switchtooptional'] = 'If you switch to the optional subscription, all currently subscribed users will be unsubscribed from this forum!'; $string['taskcleanreadrecords'] = 'Moodleoverflow maintenance job to clean old read records'; -$string['tasksenddailymail'] = 'Moodleoverflow job to send a daily mail of unread post'; -$string['tasksendmails'] = 'Moodleoverflow maintenance job to send mails'; +$string['tasksenddailymails'] = 'Moodleoverflow job to send a daily summary mail of unread posts'; +$string['tasksendmails'] = 'Moodleoverflow maintenance job to send notification mails of unread posts'; +$string['tasksendreviewmails'] = 'Moodleoverflow job to send a needed review notification mail'; $string['taskupdategrades'] = 'Moodleoverflow maintenance job to update grades'; $string['teacherrating'] = 'Solution'; $string['there_are_no_posts_needing_review'] = 'There are no more posts in this forum that need to be reviewed.'; diff --git a/lib.php b/lib.php index d8c512e6be..ac849241aa 100644 --- a/lib.php +++ b/lib.php @@ -31,6 +31,7 @@ // LEARNWEB-TODO: Adapt functions to the new way of working with posts and discussions (Replace the post/discussion functions). use core\context\course; +use mod_moodleoverflow\anonymous; defined('MOODLE_INTERNAL') || die(); require_once(dirname(__FILE__) . '/locallib.php'); @@ -586,530 +587,6 @@ function moodleoverflow_get_context($moodleoverflowid, $context = null) { return $context; } -/** - * Sends mail notifications about new posts. - * - * @return bool - */ -function moodleoverflow_send_mails() { - global $DB, $CFG, $PAGE; - - // Get the course object of the top level site. - $site = get_site(); - - // Get the main renderers. - $htmlout = $PAGE->get_renderer('mod_moodleoverflow', 'email', 'htmlemail'); - $textout = $PAGE->get_renderer('mod_moodleoverflow', 'email', 'textemail'); - - // Initiate the arrays that are saving the users that are subscribed to posts that needs sending. - $users = []; - $userscount = 0; // Count($users) is slow. This avoids using this. - - // Status arrays. - $mailcount = []; - $errorcount = []; - - // Cache arrays. - $discussions = []; - $moodleoverflows = []; - $courses = []; - $coursemodules = []; - $subscribedusers = []; - - // Posts older than x days will not be mailed. - // This will avoid problems with the cron not beeing ran for a long time. - $timenow = time(); - $endtime = $timenow - get_config('moodleoverflow', 'maxeditingtime'); - $starttime = $endtime - (get_config('moodleoverflow', 'maxmailingtime') * 60 * 60); - - // Retrieve all unmailed posts. - $posts = moodleoverflow_get_unmailed_posts($starttime, $endtime); - if ($posts) { - - // Mark those posts as mailed. - if (!moodleoverflow_mark_old_posts_as_mailed($endtime)) { - mtrace('Errors occurred while trying to mark some posts as being mailed.'); - - return false; - } - - // Loop through all posts to be mailed. - foreach ($posts as $postid => $post) { - - // Check the cache if the discussion exists. - $discussionid = $post->discussion; - if (!isset($discussions[$discussionid])) { - - // Retrieve the discussion from the database. - $discussion = $DB->get_record('moodleoverflow_discussions', ['id' => $post->discussion]); - - // If there is a record, update the cache. Else ignore the post. - if ($discussion) { - $discussions[$discussionid] = $discussion; - \mod_moodleoverflow\subscriptions::fill_subscription_cache($discussion->moodleoverflow); - \mod_moodleoverflow\subscriptions::fill_discussion_subscription_cache($discussion->moodleoverflow); - } else { - mtrace('Could not find discussion ' . $discussionid); - unset($posts[$postid]); - continue; - } - } - - // Retrieve the connected moodleoverflow instance from the database. - $moodleoverflowid = $discussions[$discussionid]->moodleoverflow; - if (!isset($moodleoverflows[$moodleoverflowid])) { - - // Retrieve the record from the database and update the cache. - $moodleoverflow = $DB->get_record('moodleoverflow', ['id' => $moodleoverflowid]); - if ($moodleoverflow) { - $moodleoverflows[$moodleoverflowid] = $moodleoverflow; - } else { - mtrace('Could not find moodleoverflow ' . $moodleoverflowid); - unset($posts[$postid]); - continue; - } - } - - // Retrieve the connected courses from the database. - $courseid = $moodleoverflows[$moodleoverflowid]->course; - if (!isset($courses[$courseid])) { - - // Retrieve the record from the database and update the cache. - $course = $DB->get_record('course', ['id' => $courseid]); - if ($course) { - $courses[$courseid] = $course; - } else { - mtrace('Could not find course ' . $courseid); - unset($posts[$postid]); - continue; - } - } - - // Retrieve the connected course modules from the database. - if (!isset($coursemodules[$moodleoverflowid])) { - - // Retrieve the coursemodule and update the cache. - $cm = get_coursemodule_from_instance('moodleoverflow', $moodleoverflowid, $courseid); - if ($cm) { - $coursemodules[$moodleoverflowid] = $cm; - } else { - mtrace('Could not find course module for moodleoverflow ' . $moodleoverflowid); - unset($posts[$postid]); - continue; - } - } - - // Cache subscribed users of each moodleoverflow. - if (!isset($subscribedusers[$moodleoverflowid])) { - - // Retrieve the context module. - $modulecontext = context_module::instance($coursemodules[$moodleoverflowid]->id); - - // Retrieve all subscribed users. - $mid = $moodleoverflows[$moodleoverflowid]; - $subusers = \mod_moodleoverflow\subscriptions::get_subscribed_users($mid, $modulecontext, 'u.*', true); - if ($subusers) { - - // Loop through all subscribed users. - foreach ($subusers as $postuser) { - - // Save the user into the cache. - $subscribedusers[$moodleoverflowid][$postuser->id] = $postuser->id; - $userscount++; - moodleoverflow_minimise_user_record($postuser); - $users[$postuser->id] = $postuser; - } - - // Release the memory. - unset($subusers); - unset($postuser); - } - } - - // Initiate the count of the mails send and errors. - $mailcount[$postid] = 0; - $errorcount[$postid] = 0; - } - } - - // Send mails to the users with information about the posts. - if ($users && $posts) { - // Send one mail to every user. - foreach ($users as $userto) { - // Terminate if the process takes more time then two minutes. - core_php_time_limit::raise(120); - - // Tracing information. - mtrace('Processing user ' . $userto->id); - // Initiate the user caches to save memory. - $userto = clone($userto); - $userto->ciewfullnames = []; - $userto->canpost = []; - $userto->markposts = []; - - // Cache the capabilities of the user. - // Check for moodle version. Version 401 supported until 8 December 2025. - if ($CFG->branch >= 402) { - \core\cron::setup_user($userto); - } else { - cron_setup_user($userto); - } - - // Reset the caches. - foreach ($coursemodules as $moodleoverflowid => $unused) { - $coursemodules[$moodleoverflowid]->cache = new stdClass(); - $coursemodules[$moodleoverflowid]->cache->caps = []; - unset($coursemodules[$moodleoverflowid]->uservisible); - } - - // Loop through all posts of this users. - foreach ($posts as $postid => $post) { - - // Initiate variables for the post. - $discussion = $discussions[$post->discussion]; - $moodleoverflow = $moodleoverflows[$discussion->moodleoverflow]; - $course = $courses[$moodleoverflow->course]; - $cm =& $coursemodules[$moodleoverflow->id]; - - // Check if user wants a resume. - // in this case: make a new dataset in "moodleoverflow_mail_info" to save the posts data. - // Dataset from moodleoverflow_mail_info will be send later in a mail. - $usermailsetting = $userto->maildigest; - if ($usermailsetting != 0) { - $dataobject = new stdClass(); - $dataobject->userid = $userto->id; - $dataobject->courseid = $course->id; - $dataobject->forumid = $moodleoverflow->id; - $dataobject->forumdiscussionid = $discussion->id; - $record = $DB->get_record('moodleoverflow_mail_info', - ['userid' => $dataobject->userid, - 'courseid' => $dataobject->courseid, - 'forumid' => $dataobject->forumid, - 'forumdiscussionid' => $dataobject->forumdiscussionid, ], - 'numberofposts, id'); - if (is_object($record)) { - $dataset = $record; - $dataobject->numberofposts = $dataset->numberofposts + 1; - $dataobject->id = $dataset->id; - $DB->update_record('moodleoverflow_mail_info', $dataobject); - } else { - $dataobject->numberofposts = 1; - $DB->insert_record('moodleoverflow_mail_info', $dataobject); - } - continue; - } - - // Check whether the user is subscribed. - if (!isset($subscribedusers[$moodleoverflow->id][$userto->id])) { - continue; - } - - // Check whether the user is subscribed to the discussion. - $iscm = $coursemodules[$moodleoverflow->id]; - $uid = $userto->id; - $did = $post->discussion; - $issubscribed = \mod_moodleoverflow\subscriptions::is_subscribed($uid, $moodleoverflow, $modulecontext, $did); - if (!$issubscribed) { - continue; - } - - // Check whether the user unsubscribed to the discussion after it was created. - $subnow = \mod_moodleoverflow\subscriptions::fetch_discussion_subscription($moodleoverflow->id, $userto->id); - if ($subnow && isset($subnow[$post->discussion]) && ($subnow[$post->discussion] > $post->created)) { - continue; - } - - if (\mod_moodleoverflow\anonymous::is_post_anonymous($discussion, $moodleoverflow, $post->userid)) { - $userfrom = \core_user::get_noreply_user(); - $userfrom->anonymous = true; - } else { - // Check whether the sending user is cached already. - if (array_key_exists($post->userid, $users)) { - $userfrom = $users[$post->userid]; - $userfrom->anonymous = false; - } else { - // We dont know the the user yet. - - // Retrieve the user from the database. - $userfrom = $DB->get_record('user', ['id' => $post->userid]); - if ($userfrom) { - moodleoverflow_minimise_user_record($userfrom); - $userfrom->anonymous = false; - } else { - $uid = $post->userid; - $pid = $post->id; - mtrace('Could not find user ' . $uid . ', author of post ' . $pid . '. Unable to send message.'); - continue; - } - } - } - - // Setup roles and languages. - // Check for moodle version. Version 401 supported until 8 December 2025. - if ($CFG->branch >= 402) { - \core\cron::setup_user($userto, $course); - } else { - cron_setup_user($userto, $course); - } - - // Cache the users capability to view full names. - if (!isset($userto->viewfullnames[$moodleoverflow->id])) { - - // Find the context module. - $modulecontext = context_module::instance($cm->id); - - // Check the users capabilities. - $userto->viewfullnames[$moodleoverflow->id] = has_capability('moodle/site:viewfullnames', $modulecontext); - } - - // Cache the users capability to post in the discussion. - if (!isset($userto->canpost[$discussion->id])) { - - // Find the context module. - $modulecontext = context_module::instance($cm->id); - - // Check the users capabilities. - $canpost = moodleoverflow_user_can_post($modulecontext, $post, $userto->id); - $userto->canpost[$discussion->id] = $canpost; - } - - // Make sure the current user is allowed to see the post. - if (!moodleoverflow_user_can_see_post($moodleoverflow, $discussion, $post, $cm)) { - mtrace('User ' . $userto->id . ' can not see ' . $post->id . '. Not sending message.'); - continue; - } - - // Sent the email. - - // Preapare to actually send the post now. Build up the content. - $cleanname = str_replace('"', "'", strip_tags(format_string($moodleoverflow->name))); - $coursecontext = context_course::instance($course->id); - $shortname = format_string($course->shortname, true, ['context' => $coursecontext]); - - // Define a header to make mails easier to track. - $emailmessageid = generate_email_messageid('moodlemoodleoverflow' . $moodleoverflow->id); - $userfrom->customheaders = [ - 'List-Id: "' . $cleanname . '" ' . $emailmessageid, - 'List-Help: ' . $CFG->wwwroot . '/mod/moodleoverflow/view.php?m=' . $moodleoverflow->id, - 'Message-ID: ' . generate_email_messageid(hash('sha256', $post->id . 'to' . $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', - ]; - - // Cache the users capabilities. - if (!isset($userto->canpost[$discussion->id])) { - $canreply = moodleoverflow_user_can_post($modulecontext, $post, $userto->id); - } else { - $canreply = $userto->canpost[$discussion->id]; - } - - // Format the data. - $data = new \mod_moodleoverflow\output\moodleoverflow_email( - $course, - $cm, - $moodleoverflow, - $discussion, - $post, - $userfrom, - $userto, - $canreply - ); - - // Retrieve the unsubscribe-link. - $userfrom->customheaders[] = sprintf('List-Unsubscribe: <%s>', $data->get_unsubscribediscussionlink()); - - // Check the capabilities to view full names. - if (!isset($userto->viewfullnames[$moodleoverflow->id])) { - $data->viewfullnames = has_capability('moodle/site:viewfullnames', $modulecontext, $userto->id); - } else { - $data->viewfullnames = $userto->viewfullnames[$moodleoverflow->id]; - } - - // Retrieve needed variables for the mail. - $var = new \stdClass(); - $var->subject = $data->get_subject(); - $var->moodleoverflowname = $cleanname; - $var->sitefullname = format_string($site->fullname); - $var->siteshortname = format_string($site->shortname); - $var->courseidnumber = $data->get_courseidnumber(); - $var->coursefullname = $data->get_coursefullname(); - $var->courseshortname = $data->get_coursename(); - $postsubject = html_to_text(get_string('postmailsubject', 'moodleoverflow', $var), 0); - $rootid = generate_email_messageid(hash('sha256', $discussion->firstpost . 'to' . $userto->id)); - - // Check whether the post is a reply. - if ($post->parent) { - - // Add a reply header. - $parentid = generate_email_messageid(hash('sha256', $post->parent . 'to' . $userto->id)); - $userfrom->customheaders[] = "In-Reply-To: $parentid"; - - // Comments need a reference to the starting post as well. - if ($post->parent != $discussion->firstpost) { - $userfrom->customheaders[] = "References: $rootid $parentid"; - } else { - $userfrom->customheaders[] = "References: $parentid"; - } - } - - // Send the post now. - mtrace('Sending ', ''); - - // Create the message event. - $eventdata = new \core\message\message(); - $eventdata->courseid = $course->id; - $eventdata->component = 'mod_moodleoverflow'; - $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; - - // Initiate another message array. - $small = new \stdClass(); - $small->user = fullname($userfrom); - $formatedstring = format_string($moodleoverflow->name, true); - $small->moodleoverflowname = "$shortname: " . $formatedstring . ": " . $discussion->name; - $small->message = $post->message; - - // Make sure the language is correct. - $usertol = $userto->lang; - $eventdata->smallmessage = get_string_manager()->get_string('smallmessage', 'moodleoverflow', $small, $usertol); - - // Generate the url to view the post. - $url = '/mod/moodleoverflow/discussion.php'; - $params = ['d' => $discussion->id]; - $contexturl = new moodle_url($url, $params, 'p' . $post->id); - $eventdata->contexturl = $contexturl->out(); - $eventdata->contexturlname = $discussion->name; - - // Actually send the message. - $mailsent = message_send($eventdata); - - // Check whether the sending failed. - if (!$mailsent) { - mtrace('Error: mod/moodleoverflow/classes/task/send_mail.php execute(): ' . - "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]++; - } - - // Tracing message. - mtrace('post ' . $post->id . ': ' . $discussion->name); - } - - // Release the memory. - unset($userto); - } - } - - // Check for all posts whether errors occurred. - if ($posts) { - - // Loop through all posts. - foreach ($posts as $post) { - - // Tracing information. - mtrace($mailcount[$post->id] . " users were sent post $post->id, '$discussion->name'"); - - // Mark the posts with errors in the database. - if ($errorcount[$post->id]) { - $DB->set_field('moodleoverflow_posts', 'mailed', MOODLEOVERFLOW_MAILED_ERROR, ['id' => $post->id]); - } - } - } - - // The task was completed. - return true; -} - -/** - * Returns a list of all posts that have not been mailed yet. - * - * @param int $starttime posts created after this time - * @param int $endtime posts created before this time - * - * @return array - */ -function moodleoverflow_get_unmailed_posts($starttime, $endtime) { - global $DB; - - // Set params for the sql query. - $params = []; - $params['ptimestart'] = $starttime; - $params['ptimeend'] = $endtime; - - $pendingmail = MOODLEOVERFLOW_MAILED_PENDING; - $reviewsent = MOODLEOVERFLOW_MAILED_REVIEW_SUCCESS; - - // Retrieve the records. - $sql = "SELECT p.*, d.course, d.moodleoverflow - FROM {moodleoverflow_posts} p - JOIN {moodleoverflow_discussions} d ON d.id = p.discussion - WHERE p.mailed IN ($pendingmail, $reviewsent) AND p.reviewed = 1 - AND COALESCE(p.timereviewed, p.created) >= :ptimestart AND p.created < :ptimeend - ORDER BY p.modified ASC"; - - return $DB->get_records_sql($sql, $params); -} - -/** - * Marks posts before a certain time as being mailed already. - * - * @param int $endtime - * - * @return bool - */ -function moodleoverflow_mark_old_posts_as_mailed($endtime) { - global $DB; - - // Get the current timestamp. - $now = time(); - - // Define variables for the sql query. - $params = []; - $params['mailedsuccess'] = MOODLEOVERFLOW_MAILED_SUCCESS; - $params['mailedreviewsent'] = MOODLEOVERFLOW_MAILED_REVIEW_SUCCESS; - $params['now'] = $now; - $params['endtime'] = $endtime; - $params['mailedpending'] = MOODLEOVERFLOW_MAILED_PENDING; - - // Define the sql query. - $sql = "UPDATE {moodleoverflow_posts} - SET mailed = :mailedsuccess - WHERE (created < :endtime) AND mailed IN (:mailedpending, :mailedreviewsent) AND reviewed = 1"; - - return $DB->execute($sql, $params); - -} - -/** - * Removes unnecessary information from the user records for the mail generation. - * - * @param stdClass $user - */ -function moodleoverflow_minimise_user_record(stdClass &$user) { - - // Remove all information for the mail generation that are not needed. - unset($user->institution); - unset($user->department); - unset($user->address); - unset($user->city); - unset($user->url); - unset($user->currentlogin); - unset($user->description); - unset($user->descriptionformat); -} - /** * Adds information about unread messages, that is only required for the course view page (and * similar), to the course-module object. diff --git a/locallib.php b/locallib.php index 44b3a381d5..79fde129e7 100644 --- a/locallib.php +++ b/locallib.php @@ -901,7 +901,7 @@ function moodleoverflow_go_back_to($default) { /** * Checks whether the user can reply to posts in a discussion. * - * @param object $modulecontext + * @param context $modulecontext * @param object $posttoreplyto * @param bool $considerreviewstatus * @param int $userid @@ -1751,7 +1751,7 @@ function moodleoverflow_add_new_post($post) { // Set to not reviewed, if posts should be reviewed, and user is not a reviewer themselves. if (review::get_review_level($moodleoverflow) == review::EVERYTHING && - !has_capability('mod/moodleoverflow:reviewpost', context_module::instance($cm->id))) { + !has_capability('mod/moodleoverflow:reviewpost', $context)) { $post->reviewed = 0; } else { $post->reviewed = 1; diff --git a/settings.php b/settings.php index bb2e60dd27..73941449f6 100644 --- a/settings.php +++ b/settings.php @@ -137,7 +137,7 @@ $setting->set_updatedcallback('moodleoverflow_update_all_grades'); } - // Allow teachers to see cummulative userstats. + // Allow teachers to see cumulative userstats. $settings->add(new admin_setting_configcheckbox('moodleoverflow/showuserstats', get_string('showuserstats', 'moodleoverflow'), get_string('configshowuserstats', 'moodleoverflow'), 0)); diff --git a/tests/dailymail_test.php b/tests/dailymail_test.php index d0a32a3ee2..9ac3c1e39d 100644 --- a/tests/dailymail_test.php +++ b/tests/dailymail_test.php @@ -24,7 +24,8 @@ namespace mod_moodleoverflow; use mod_moodleoverflow\task\send_mails; -use mod_moodleoverflow\task\send_daily_mail; +use mod_moodleoverflow\task\send_daily_mails; +use stdClass; defined('MOODLE_INTERNAL') || die(); @@ -38,30 +39,19 @@ * @package mod_moodleoverflow * @copyright 2023 Tamaro Walter * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @covers \mod_moodleoverflow\task\send_daily_mail::execute + * @covers \mod_moodleoverflow\task\send_daily_mails::execute */ final class dailymail_test extends \advanced_testcase { - /** @var \stdClass collection of messages */ - private $sink; - /** @var \stdClass test course */ - private $course; - - /** @var \stdClass test user*/ - private $user; - - /** @var \stdClass moodleoverflow instance */ - private $moodleoverflow; - - /** @var \stdClass coursemodule instance */ - private $coursemodule; - - /** @var \stdClass discussion instance */ - private $discussion; - - /** @var \component_generator_base moodleoverflow generator */ - private $generator; + /** @var stdClass test environment + * This Class contains the test environment: + * - the message sink to check if mails were sent. + * - a course, moodleoverflow, coursemodule (cm) and discussion instance. + * - a teacher and a student user. + * - the moodleoverflow generator. + */ + private $env; /** * Test setUp. @@ -69,17 +59,18 @@ final class dailymail_test extends \advanced_testcase { public function setUp(): void { parent::setUp(); $this->resetAfterTest(); - set_config('maxeditingtime', -10, 'moodleoverflow'); + $this->env = new stdClass(); + set_config('maxeditingtime', -10, 'moodleoverflow'); unset_config('noemailever'); - $this->sink = $this->redirectEmails(); + $this->env->sink = $this->redirectEmails(); $this->preventResetByRollback(); - $this->redirectMessages(); + // Create a new course with a moodleoverflow forum. - $this->course = $this->getDataGenerator()->create_course(); - $location = ['course' => $this->course->id, 'forcesubscribe' => MOODLEOVERFLOW_FORCESUBSCRIBE]; - $this->moodleoverflow = $this->getDataGenerator()->create_module('moodleoverflow', $location); - $this->coursemodule = get_coursemodule_from_instance('moodleoverflow', $this->moodleoverflow->id); + $this->env->course = $this->getDataGenerator()->create_course(); + $location = ['course' => $this->env->course->id, 'forcesubscribe' => MOODLEOVERFLOW_FORCESUBSCRIBE]; + $this->env->moodleoverflow = $this->getDataGenerator()->create_module('moodleoverflow', $location); + $this->env->coursemodule = get_coursemodule_from_instance('moodleoverflow', $this->env->moodleoverflow->id); } /** @@ -87,8 +78,8 @@ public function setUp(): void { */ public function tearDown(): void { // Clear all caches. - \mod_moodleoverflow\subscriptions::reset_moodleoverflow_cache(); - \mod_moodleoverflow\subscriptions::reset_discussion_cache(); + subscriptions::reset_moodleoverflow_cache(); + subscriptions::reset_discussion_cache(); parent::tearDown(); } @@ -98,83 +89,75 @@ public function tearDown(): void { * Function that creates a new user, which adds a new discussion an post to the moodleoverflow. * @param int $maildigest The maildigest setting: 0 = off , 1 = on */ - public function helper_create_user_and_discussion($maildigest) { + public function helper_test_set_up($maildigest) { + $this->env->generator = $this->getDataGenerator(); // Create a user enrolled in the course as student. - $this->user = $this->getDataGenerator()->create_user(['firstname' => 'Tamaro', 'email' => 'tamaromail@example.com', + $this->env->teacher = $this->env->generator->create_user(['firstname' => 'Tamaro', 'email' => 'tamaromail@example.com', 'maildigest' => $maildigest, ]); - $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, 'student'); + $this->env->student = $this->env->generator->create_user(['firstname' => 'Student1', 'email' => 'student1mail@example.com', + 'maildigest' => $maildigest, ]); + $this->env->generator->enrol_user($this->env->teacher->id, $this->env->course->id, 'teacher'); + $this->env->generator->enrol_user($this->env->student->id, $this->env->course->id, 'teacher'); // Create a new discussion and post within the moodleoverflow. - $this->generator = $this->getDataGenerator()->get_plugin_generator('mod_moodleoverflow'); - $this->discussion = $this->generator->post_to_forum($this->moodleoverflow, $this->user); + $this->env->plugingenerator = $this->env->generator->get_plugin_generator('mod_moodleoverflow'); + $this->env->discussions = $this->env->plugingenerator->post_to_forum($this->env->moodleoverflow, $this->env->student); } /** * Run the send daily mail task. * @return false|string */ - private function helper_run_send_daily_mail() { - $mailtask = new send_daily_mail(); + private function helper_run_send_daily_mails() { + $dailymailtask = new send_daily_mails(); + $notificationmailtask = new send_mails(); + $notificationmailtask->execute(); ob_start(); - $mailtask->execute(); + $dailymailtask->execute(); $output = ob_get_contents(); ob_end_clean(); return $output; } - /** - * Run the send mails task. - * @return false|string - */ - private function helper_run_send_mails() { - $mailtask = new send_mails(); - ob_start(); - $mailtask->execute(); - $output = ob_get_contents(); - ob_end_clean(); - return $output; - } - - - // Begin of test functions. /** - * Test if the task send_daily_mail sends a mail to the user. + * Test if the task send_daily_mails sends a mail to the user. */ public function test_mail_delivery(): void { // Create user with maildigest = on. - $this->helper_create_user_and_discussion('1'); + $this->helper_test_set_up('1'); // Send a mail and test if the mail was sent. - $this->helper_run_send_mails(); - $this->helper_run_send_daily_mail(); - $messages = $this->sink->count(); + $this->helper_run_send_daily_mails(); + $messages = $this->env->sink->count(); $this->assertEquals(1, $messages); } /** - * Test if the task send_daily_mail does not sends email from posts that are not in the course of the user. + * Test if the task send_daily_mails does not sends email from posts that are not in the course of the user. */ public function test_delivery_not_enrolled(): void { // Create user with maildigest = on. - $this->helper_create_user_and_discussion('1'); + $this->helper_test_set_up('1'); // Create another user, course and a moodleoverflow post. - $course = $this->getDataGenerator()->create_course(); + $course = $this->env->generator->create_course(); $location = ['course' => $course->id, 'forcesubscribe' => MOODLEOVERFLOW_FORCESUBSCRIBE]; - $moodleoverflow = $this->getDataGenerator()->create_module('moodleoverflow', $location); - $student = $this->getDataGenerator()->create_user(['firstname' => 'Ethan', 'email' => 'ethanmail@example.com', + $moodleoverflow = $this->env->generator->create_module('moodleoverflow', $location); + $student = $this->env->generator->create_user(['firstname' => 'Student2', 'email' => 'student2@example.com', 'maildigest' => '1', ]); - $this->getDataGenerator()->enrol_user($student->id, $course->id, 'teacher'); - $discussion = $this->generator->post_to_forum($moodleoverflow, $student); + $teacher = $this->env->generator->create_user(['firstname' => 'Teacher2', 'email' => 'teacher2@example.com', + 'maildigest' => '1', ]); + $this->env->generator->enrol_user($student->id, $course->id, 'student'); + $this->env->generator->enrol_user($teacher->id, $course->id, 'teacher'); + $discussion = $this->env->plugingenerator->post_to_forum($moodleoverflow, $student); // Send the mails. - $this->helper_run_send_mails(); - $this->helper_run_send_daily_mail(); - $messages = $this->sink->count(); - $content = $this->sink->get_messages(); + $this->helper_run_send_daily_mails(); + $messages = $this->env->sink->count(); + $content = $this->env->sink->get_messages(); // There should be 2 mails. $this->assertEquals(2, $messages); @@ -184,17 +167,17 @@ public function test_delivery_not_enrolled(): void { $secondmail = $content[1]; // Depending on the order of the mails, check the recipient and the discussion that is addressed. if ($firstmail->to == "tamaromail@example.com") { - $this->assertStringContainsString($this->discussion[0]->name, $firstmail->body); + $this->assertStringContainsString($this->env->discussions[0]->name, $firstmail->body); $this->assertStringNotContainsString($discussion[0]->name, $firstmail->body); - $this->assertEquals('ethanmail@example.com', $secondmail->to); + $this->assertEquals('teacher2@example.com', $secondmail->to); $this->assertStringContainsString($discussion[0]->name, $secondmail->body); - $this->assertStringNotContainsString($this->discussion[0]->name, $secondmail->body); + $this->assertStringNotContainsString($this->env->discussions[0]->name, $secondmail->body); } else { - $this->assertEquals('ethanmail@example.com', $firstmail->to); + $this->assertEquals('teacher2@example.com', $firstmail->to); $this->assertStringContainsString($discussion[0]->name, $firstmail->body); - $this->assertStringNotContainsString($this->discussion[0]->name, $firstmail->body); + $this->assertStringNotContainsString($this->env->discussions[0]->name, $firstmail->body); $this->assertEquals('tamaromail@example.com', $secondmail->to); - $this->assertStringContainsString($this->discussion[0]->name, $secondmail->body); + $this->assertStringContainsString($this->env->discussions[0]->name, $secondmail->body); $this->assertStringNotContainsString($discussion[0]->name, $secondmail->body); } } @@ -206,22 +189,21 @@ public function test_delivery_not_enrolled(): void { public function test_content_of_mail_delivery(): void { // Create user with maildigest = on. - $this->helper_create_user_and_discussion('1'); + $this->helper_test_set_up('1'); // Send the mails and count the messages. - $this->helper_run_send_mails(); - $this->helper_run_send_daily_mail(); - $content = $this->sink->get_messages(); + $this->helper_run_send_daily_mails(); + $content = $this->env->sink->get_messages(); $message = $content[0]->body; $message = str_replace(["\n\r", "\n", "\r"], '', $message); - $messagecount = $this->sink->count(); + $messagecount = $this->env->sink->count(); // Build the text that the mail should have. // Text structure at get_string('digestunreadpost', moodleoverflow). - $linktocourse = 'course->id; - $linktoforum = 'coursemodule->id; + $linktocourse = 'env->course->id; + $linktoforum = 'env->coursemodule->id; $linktodiscussion = 'discussion[0]->id; + . $this->env->discussions[0]->id; $this->assertStringContainsString($linktocourse, $message); $this->assertStringContainsString($linktoforum, $message); @@ -235,14 +217,14 @@ public function test_content_of_mail_delivery(): void { */ public function test_mail_not_send(): void { // Creat user with daily_mail = off. - $this->helper_create_user_and_discussion('0'); + $this->helper_test_set_up('0'); // Now send the mails and test if no mail was sent. - $this->helper_run_send_mails(); - $this->helper_run_send_daily_mail(); - $messages = $this->sink->count(); + $this->helper_run_send_daily_mails(); + $messages = $this->env->sink->get_messages()[0]; - $this->assertEquals(0, $messages); + // The teacher now gets a notification mail. The subject of the mail is now different. + $this->assertNotEquals($messages->subject, get_string('tasksenddailymails', 'mod_moodleoverflow')); } /** @@ -251,14 +233,13 @@ public function test_mail_not_send(): void { public function test_records_removed(): void { global $DB; // Create user with maildigest = on. - $this->helper_create_user_and_discussion('1'); + $this->helper_test_set_up('1'); // Now send the mails. - $this->helper_run_send_mails(); - $this->helper_run_send_daily_mail(); + $this->helper_run_send_daily_mails(); // Now check the database if the records of the users are deleted. - $records = $DB->get_records('moodleoverflow_mail_info', ['userid' => $this->user->id]); + $records = $DB->get_records('moodleoverflow_mail_info', ['userid' => $this->env->teacher->id]); $this->assertEmpty($records); } } diff --git a/tests/generator/lib.php b/tests/generator/lib.php index c27be6dad5..c8d2873704 100644 --- a/tests/generator/lib.php +++ b/tests/generator/lib.php @@ -91,15 +91,6 @@ public function create_instance($record = null, ?array $options = null) { return parent::create_instance($record, (array) $options); } - /** - * Creates a moodleoverflow discussion. - * - * @param null $record - * - * @return bool|int - * @throws coding_exception - */ - /** * Creates a moodleoverflow discussion. * diff --git a/tests/notification_mail_test.php b/tests/notification_mail_test.php new file mode 100644 index 0000000000..c5cb4b0fd2 --- /dev/null +++ b/tests/notification_mail_test.php @@ -0,0 +1,147 @@ +. + +namespace mod_moodleoverflow; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/completionlib.php'); + +use mod_moodleoverflow\manager\mail_manager; +use mod_moodleoverflow\subscriptions; +use mod_moodleoverflow\task\send_mails; +use PHPUnit\Exception; +use PHPUnit\Framework\Attributes\CoversClass; +use stdClass; + +/** + * Unit tests for the mod_moodleoverflow plugin. + * + * @package mod_moodleoverflow + * @copyright 2025 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * PHPUnit tests for testing the process of sending notification of new posts via email. + * + * @package mod_moodleoverflow + * @copyright 2025 Tamaro Walter + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \mod_moodleoverflow\manager\mail_manager + */ +#[CoversClass(mail_manager::class)] +final class notification_mail_test extends \advanced_testcase { + + // Attributes. + + /** @var object The data that will be used for testing. + * This Class contains the test data: + * - one course. + * - a moodleoverflow activity + * - a teacher. + * - two students. + * - message and mail sinks to check if mails were sent. + */ + private $testdata; + + // Construct functions. + public function setUp(): void { + parent::setUp(); + $this->testdata = new stdClass(); + $this->resetAfterTest(); + $this->helper_course_set_up(); + } + + public function tearDown(): void { + $this->testdata = null; + parent::tearDown(); + } + + // Tests. + + /** + * Test if order of the mails is correct. + * + * @return void + * @covers \mod_moodleoverflow\task\send_mails + */ + public function test_sortorder(): void { + global $DB; + $result = $this->helper_run_task(); + $this->assertTrue(true); + /* LEARNWEB-TODO: Add tests. A simple test coverage of the notification mails are in review_test.php for now. + They need to be removed from there and added here (+extending test cases). */ + } + + // Helper functions. + + /** + * Helper function that creates: + * - two courses. + * - an assignment in each course. + * - an activity completion in the first course. + * - a teacher that is enrolled in both courses. + * - a student in each course. + */ + private function helper_course_set_up(): void { + global $DB; + $datagenerator = $this->getDataGenerator(); + $plugingenerator = $datagenerator->get_plugin_generator('mod_moodleoverflow'); + $this->testdata->mailsink = $this->redirectEmails(); + $this->testdata->messagesink = $this->redirectMessages(); + // Create a new course. + $this->testdata->course = $datagenerator->create_course(); + + // Create a teacher and a student and enroll them in the course. + $this->testdata->teacher = $datagenerator->create_user(['firstname' => 'Tamaro', 'email' => 'tamaromail@example.com', + 'maildigest' => 0, ]); + $this->testdata->student1 = $datagenerator->create_user(['firstname' => 'Student1', 'email' => 'student1mail@example.com', + 'maildigest' => 0, ]); + $this->testdata->student2 = $datagenerator->create_user(['firstname' => 'Student1', 'email' => 'student1mail@example.com', + 'maildigest' => 0, ]); + + $datagenerator->enrol_user($this->testdata->teacher->id, $this->testdata->course->id, 'teacher'); + $datagenerator->enrol_user($this->testdata->student1->id, $this->testdata->course->id, 'student'); + $datagenerator->enrol_user($this->testdata->student2->id, $this->testdata->course->id, 'student'); + + // Change configs so that mails will be sent immediately. + set_config('reviewpossibleaftertime', -10, 'moodleoverflow'); + set_config('maxeditingtime', -10, 'moodleoverflow'); + unset_config('noemailever'); + + // Create a moodleoverflow with a discussion from the teacher. + $options = ['course' => $this->testdata->course->id, 'forcesubscribe' => MOODLEOVERFLOW_FORCESUBSCRIBE]; + $this->testdata->moodleoverflow = $datagenerator->create_module('moodleoverflow', $options); + $this->testdata->coursemodule = get_coursemodule_from_instance('moodleoverflow', $this->testdata->moodleoverflow->id); + $this->testdata->discussion = $plugingenerator->post_to_forum($this->testdata->moodleoverflow, $this->testdata->teacher); + } + + /** + * Runs the task to send notification mails. + * @return false|string + */ + private function helper_run_task() { + $mailtask = new send_mails(); + ob_start(); + $mailtask->execute(); + $this->testdata->output = ob_get_contents(); + ob_end_clean(); + return false; + } +} diff --git a/tests/review_test.php b/tests/review_test.php index 4ef36a3f70..dce3b3f9ef 100644 --- a/tests/review_test.php +++ b/tests/review_test.php @@ -24,7 +24,9 @@ namespace mod_moodleoverflow; +use mod_moodleoverflow\manager\mail_manager; use mod_moodleoverflow\task\send_mails; +use mod_moodleoverflow\task\send_review_mails; defined('MOODLE_INTERNAL') || die(); @@ -62,10 +64,7 @@ final class review_test extends \advanced_testcase { * @var \phpunit_message_sink */ private $mailsink; - /** - * @var \phpunit_message_sink - */ - private $messagesink; + /** * set Up testing data. @@ -87,8 +86,6 @@ protected function setUp(): void { unset_config('noemailever'); $this->mailsink = $this->redirectEmails(); - - $this->messagesink = $this->redirectMessages(); } /** @@ -99,10 +96,6 @@ protected function tearDown(): void { $this->mailsink->clear(); $this->mailsink->close(); unset($this->mailsink); - - $this->messagesink->clear(); - $this->messagesink->close(); - unset($this->messagesink); parent::tearDown(); } @@ -121,45 +114,46 @@ public function test_forum_review_everything(): void { $posts = $this->create_post($options); $this->check_mail_records($posts['teacherpost'], $posts['studentpost'], 1, 0, MOODLEOVERFLOW_MAILED_REVIEW_SUCCESS); - $this->assertEquals(1, $this->mailsink->count()); // Teacher has to approve student message. - $this->assertEquals(2, $this->messagesink->count()); // Student and teacher get notification for student message. + // There should be one review mail for the teacher. + // And 1 notification mail for the student (teacher does not get a notification mail for his own post). + $this->assertEquals(2, $this->mailsink->count()); // Teacher has to approve student message. $this->mailsink->clear(); - $this->messagesink->clear(); $this->assertNull(\mod_moodleoverflow_external::review_approve_post($posts['studentpost']->id)); - $this->run_send_mails(); - $this->run_send_mails(); // Execute twice to ensure no duplicate mails. + $this->run_send_notification_mails(); + $this->run_send_review_mails(); $post = $DB->get_record('moodleoverflow_posts', ['id' => $posts['studentpost']->id]); $this->assert_matches_properties(['mailed' => MOODLEOVERFLOW_MAILED_SUCCESS, 'reviewed' => 1], $post); $this->assertNotNull($post->timereviewed ?? null); - $this->assertEquals(0, $this->mailsink->count()); - $this->assertEquals(2, $this->messagesink->count()); - - $this->messagesink->clear(); + // There should be one notification mail for the approved post. + $this->assertEquals(1, $this->mailsink->count()); + $this->mailsink->clear(); + $this->setUser($this->student); $studentanswer1 = $this->generator->reply_to_post($posts['teacherpost'], $this->student, false); $studentanswer2 = $this->generator->reply_to_post($posts['teacherpost'], $this->student, false); + $this->setAdminUser(); - $this->run_send_mails(); - $this->run_send_mails(); // Execute twice to ensure no duplicate mails. + $this->run_send_notification_mails(); + $this->run_send_review_mails(); + // There should be two review mails for the teacher. $this->assertEquals(2, $this->mailsink->count()); - $this->assertEquals(0, $this->messagesink->count()); $this->mailsink->clear(); $this->assertNotNull(\mod_moodleoverflow_external::review_approve_post($studentanswer1->id)); $this->assertNull(\mod_moodleoverflow_external::review_reject_post($studentanswer2->id, 'This post was not good!')); - $this->run_send_mails(); - $this->run_send_mails(); // Execute twice to ensure no duplicate mails. + $this->run_send_notification_mails(); + $this->run_send_review_mails(); - $this->assertEquals(1, $this->mailsink->count()); - $this->assertEquals(2, $this->messagesink->count()); + // One review mail for the teacher and one notification mail for the student. + $this->assertEquals(2, $this->mailsink->count()); $rejectionmessage = $this->mailsink->get_messages()[0]; @@ -185,33 +179,27 @@ public function test_forum_review_only_questions(): void { $posts = $this->create_post($options); $this->check_mail_records($posts['teacherpost'], $posts['studentpost'], 1, 0, MOODLEOVERFLOW_MAILED_REVIEW_SUCCESS); - $this->assertEquals(1, $this->mailsink->count()); // Teacher has to approve student message. - $this->assertEquals(2, $this->messagesink->count()); // Student and teacher get notification for student message. + // There should be one review needed mail for the teacher and one notification mail for the student. + $this->assertEquals(2, $this->mailsink->count()); $this->mailsink->clear(); - $this->messagesink->clear(); $this->assertNull(\mod_moodleoverflow_external::review_approve_post($posts['studentpost']->id)); - $this->run_send_mails(); - $this->run_send_mails(); // Execute twice to ensure no duplicate mails. + $this->run_send_notification_mails(); + $this->run_send_review_mails(); $post = $DB->get_record('moodleoverflow_posts', ['id' => $posts['studentpost']->id]); $this->assert_matches_properties(['mailed' => MOODLEOVERFLOW_MAILED_SUCCESS, 'reviewed' => 1], $post); $this->assertNotNull($post->timereviewed ?? null); - $this->assertEquals(0, $this->mailsink->count()); - $this->assertEquals(2, $this->messagesink->count()); - - $this->messagesink->clear(); + // There should be one notification mail for the student for the approved post. + $this->assertEquals(1, $this->mailsink->count()); $studentanswer1 = $this->generator->reply_to_post($posts['teacherpost'], $this->student, false); $studentanswer2 = $this->generator->reply_to_post($posts['teacherpost'], $this->student, false); $this->check_mail_records($studentanswer1, $studentanswer2, 1, 1, MOODLEOVERFLOW_MAILED_SUCCESS); - - $this->assertEquals(0, $this->mailsink->count()); - $this->assertEquals(4, $this->messagesink->count()); } /** @@ -226,26 +214,37 @@ public function test_forum_review_disallowed(): void { $posts = $this->create_post($options); $this->check_mail_records($posts['teacherpost'], $posts['studentpost'], 1, 1, MOODLEOVERFLOW_MAILED_SUCCESS); - $this->assertEquals(0, $this->mailsink->count()); // Teacher has to approve student message. - $this->assertEquals(4, $this->messagesink->count()); // Student and teacher get notification for student message. + // There should be 2 notifications mails (one for each post). + $this->assertEquals(2, $this->mailsink->count()); // Teacher has to approve student message. $this->mailsink->clear(); - $this->messagesink->clear(); + $this->setUser($this->student); $studentanswer1 = $this->generator->reply_to_post($posts['teacherpost'], $this->student, false); $studentanswer2 = $this->generator->reply_to_post($posts['teacherpost'], $this->student, false); + $this->setAdminUser(); $this->check_mail_records($studentanswer1, $studentanswer2, 1, 1, MOODLEOVERFLOW_MAILED_SUCCESS); + } - $this->assertEquals(0, $this->mailsink->count()); - $this->assertEquals(4, $this->messagesink->count()); + /** + * Run the send mails task. + * @return false|string + */ + private function run_send_review_mails() { + $mailtask = new send_review_mails(); + ob_start(); + $mailtask->execute(); + $output = ob_get_contents(); + ob_end_clean(); + return $output; } /** * Run the send mails task. * @return false|string */ - private function run_send_mails() { + private function run_send_notification_mails() { $mailtask = new send_mails(); ob_start(); $mailtask->execute(); @@ -307,8 +306,8 @@ private function check_mail_records($teacherpost, $studentpost, $review1, $revie 'reviewed' => $review2, 'timereviewed' => null, ], $DB->get_record('moodleoverflow_posts', ['id' => $studentpost->id])); - $this->run_send_mails(); - $this->run_send_mails(); // Execute twice to ensure no duplicate mails. + $this->run_send_notification_mails(); + $this->run_send_review_mails(); $this->assert_matches_properties(['mailed' => MOODLEOVERFLOW_MAILED_SUCCESS, 'reviewed' => $review1, 'timereviewed' => null, ], diff --git a/version.php b/version.php index 6f099b71c6..15e7ad3556 100644 --- a/version.php +++ b/version.php @@ -26,7 +26,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2025070700; +$plugin->version = 2025072500; $plugin->requires = 2022112819; // Require Moodle 4.1. $plugin->supported = [401, 500]; $plugin->component = 'mod_moodleoverflow';