From 6f0f979ae3069ea34747ed702bfdc0f7fb79ac63 Mon Sep 17 00:00:00 2001 From: Juan Leyva Date: Wed, 28 Jul 2021 15:26:20 +0200 Subject: [PATCH] MDL-70083 message: Updated content notification task --- .../task/content_notification_task.php | 154 ++++++++++++++++++ course/modlib.php | 14 +- course/moodleform_mod.php | 5 +- course/tests/notifications_test.php | 120 ++++++++++++++ lang/en/course.php | 6 + 5 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 course/classes/task/content_notification_task.php create mode 100644 course/tests/notifications_test.php diff --git a/course/classes/task/content_notification_task.php b/course/classes/task/content_notification_task.php new file mode 100644 index 00000000000..3f681f81e8b --- /dev/null +++ b/course/classes/task/content_notification_task.php @@ -0,0 +1,154 @@ +. + +/** + * Class handling course content updates notifications. + * + * @package core_course + * @copyright 2021 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_course\task; + +use core\task\adhoc_task; + +/** + * Class handling course content updates notifications. + * + * @package core_course + * @copyright 2021 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class content_notification_task extends adhoc_task { + + // Use the logging trait for better logging. + use \core\task\logging_trait; + + /** + * Run the main task. + * + * @throws \coding_exception if something wrong happens. + */ + public function execute() { + global $CFG, $OUTPUT; + require_once($CFG->libdir . '/enrollib.php'); + + $data = $this->get_custom_data(); + + $course = get_course($data->courseid); + $modinfo = get_fast_modinfo($course); + $cm = $modinfo->cms[$data->cmid]; + $isupdate = !empty($data->update); + + if (!$course->visible || !$cm->visible) { + // The course or module is hidden. We don't check if the user can see hidden courses, does not make sense here. + // Permissions may have changed since it was queued. + return; + } + + // Get only active users. + $coursecontext = \context_course::instance($course->id); + $modcontext = \context_module::instance($cm->id); + $users = get_enrolled_users($coursecontext, '', 0, 'u.*', null, 0, 0, true); + + if (empty($users)) { + return; + } + + $userfrom = \core_user::get_user($data->userfrom); + + // Now send the messages + $countusers = count($users); + $sentcount = $errorcount = 0; + $this->log_start("Sending course content update notifications to {$countusers} potential users + from user with id {$userfrom->id}."); + foreach ($users as $user) { + + cron_setup_user($user, $course); + + $cm = get_fast_modinfo($course)->cms[$cm->id]; + + if (!$cm->uservisible && !$cm->is_visible_on_course_page()) { + // User can't access or see the activity in the course page. + $this->log("Ignoring user {$user->id} (no permissions to see the module)", 1); + continue; + } + + // Get module names in the user's language. + $modnames = get_module_types_names(); + $a = [ + 'coursename' => get_course_display_name_for_list($course), + 'courselink' => (new \moodle_url('/course/view.php', ['id' => $course->id]))->out(false), + 'modulename' => format_string($cm->name, $modcontext->id), + 'moduletypename' => $modnames[$cm->modname], + 'link' => (new \moodle_url('/mod/' . $cm->modname . '/view.php', ['id' => $cm->id]))->out(false), + 'notificationpreferenceslink' => + (new \moodle_url('/message/notificationpreferences.php', ['userid' => $user->id]))->out(false), + ]; + + if ($isupdate) { + $messagesubject = get_string('coursecontentnotifupdate', 'course', $a); + $messagebody = get_string('coursecontentnotifupdatebody', 'course', $a); + } else { + $messagesubject = get_string('coursecontentnotifnew', 'course', $a); + $messagebody = get_string('coursecontentnotifnewbody', 'course', $a); + } + + // Send notification. + $eventdata = new \core\message\message(); + $eventdata->courseid = $course->id; + $eventdata->component = 'moodle'; + $eventdata->name = 'coursecontentupdated'; + $eventdata->userfrom = $userfrom; + $eventdata->userto = $user; + $eventdata->subject = $messagesubject; + $eventdata->fullmessageformat = FORMAT_HTML; + $eventdata->fullmessagehtml = $messagebody; + $eventdata->smallmessage = strip_tags($eventdata->fullmessagehtml); + $eventdata->contexturl = (new \moodle_url('/mod/' . $cm->modname . '/view.php', ['id' => $cm->id]))->out(false); + $eventdata->contexturlname = $cm->name; + $eventdata->notification = 1; + $eventdata->customdata = [ + 'notificationiconurl' => $cm->get_icon_url()->out(false), + ]; + if ($courseimage = \core_course\external\course_summary_exporter::get_course_image($course)) { + $eventdata->customdata['notificationpictureurl'] = $courseimage; + } + + $completion = \core_completion\cm_completion_details::get_instance($cm, $user->id); + $activitydates = \core\activity_dates::get_dates_for_module($cm, $user->id); + if (!empty($activitydates)) { + $activityinfo = new \core_course\output\activity_information($cm, $completion, $activitydates); + $data = $activityinfo->export_for_template($OUTPUT); + foreach ($data->activitydates as $date) { + $eventdata->fullmessagehtml .= \html_writer::div($date['label'] . ' ' . $date['datestring']); + } + } + $eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml); + + if (message_send($eventdata)) { + $this->log("Notification sent to user with id {$user->id}", 1); + $sentcount++; + } else { + $this->log("Failed to send notification to user with id {$user->id}", 1); + $errorcount++; + } + } + + $this->log_finish("Sent {$sentcount} notifications with {$errorcount} failures"); + } +} diff --git a/course/modlib.php b/course/modlib.php index 80e24b7b614..bcf46222f8d 100644 --- a/course/modlib.php +++ b/course/modlib.php @@ -213,7 +213,7 @@ function plugin_extend_coursemodule_edit_post_actions($moduleinfo, $course) { * @return object moduleinfo update with grading management info */ function edit_module_post_actions($moduleinfo, $course) { - global $CFG; + global $CFG, $USER; require_once($CFG->libdir.'/gradelib.php'); $modcontext = context_module::instance($moduleinfo->coursemodule); @@ -390,6 +390,18 @@ function edit_module_post_actions($moduleinfo, $course) { // Allow plugins to extend the course module form. $moduleinfo = plugin_extend_coursemodule_edit_post_actions($moduleinfo, $course); + if (!empty($moduleinfo->coursecontentnotification)) { + // Schedule adhoc-task for delivering the course content updated notification. + if ($course->visible && $moduleinfo->visible) { + $adhocktask = new \core_course\task\content_notification_task(); + $adhocktask->set_custom_data( + ['update' => $moduleinfo->update, 'cmid' => $moduleinfo->coursemodule, + 'courseid' => $course->id, 'userfrom' => $USER->id]); + $adhocktask->set_component('course'); + \core\task\manager::queue_adhoc_task($adhocktask, true); + } + } + return $moduleinfo; } diff --git a/course/moodleform_mod.php b/course/moodleform_mod.php index f444d47eeca..d9018c16bb4 100644 --- a/course/moodleform_mod.php +++ b/course/moodleform_mod.php @@ -1190,6 +1190,10 @@ abstract class moodleform_mod extends moodleform { $mform = $this->_form; + $mform->addElement('checkbox', 'coursecontentnotification', get_string('coursecontentnotification', 'course')); + $mform->addHelpButton('coursecontentnotification', 'coursecontentnotification', 'course'); + $mform->closeHeaderBefore('coursecontentnotification'); + // elements in a row need a group $buttonarray = array(); @@ -1209,7 +1213,6 @@ abstract class moodleform_mod extends moodleform { $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); $mform->setType('buttonar', PARAM_RAW); - $mform->closeHeaderBefore('buttonar'); } /** diff --git a/course/tests/notifications_test.php b/course/tests/notifications_test.php new file mode 100644 index 00000000000..951d8b625e0 --- /dev/null +++ b/course/tests/notifications_test.php @@ -0,0 +1,120 @@ +. + +namespace tests\core_course; + +/** + * Contains tests for course related notifications. + * + * @package core + * @subpackage course + * @copyright 2021 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class notifications_test extends \advanced_testcase { + + /** + * Test coursecontentupdated class. + */ + public function test_coursecontentupdated_notifications() { + global $DB; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = self::getDataGenerator()->create_course(); + // Enrol couple of students to receive a notification and one unactive enrolment. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + $user3 = self::getDataGenerator()->create_user(); + self::getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); + self::getDataGenerator()->enrol_user($user2->id, $course->id, 'student'); + self::getDataGenerator()->enrol_user($user3->id, $course->id, 'student', 'manual', time() - YEARSECS, time() - WEEKSECS); + + $url = self::getDataGenerator()->create_module('url', ['course' => $course]); + + // Test update. + $moduleinfo = $DB->get_record('course_modules', array('id' => $url->cmid)); + $moduleinfo->modulename = 'url'; + $moduleinfo->coursemodule = $url->cmid; + $moduleinfo->display = 1; + $moduleinfo->externalurl = ''; + $moduleinfo->update = 1; + $draftid = 0; + file_prepare_draft_area($draftid, \context_module::instance($url->cmid)->id, 'mod_url', 'intro', 0); + $moduleinfo->introeditor = [ + 'itemid' => $draftid, + 'text' => '

Yo

', + 'format' => FORMAT_HTML + ]; + $modurl = (new \moodle_url('/mod/url/view.php', ['id' => $url->cmid]))->out(false); + + // Check course content changed notifications. + $moduleinfo->coursecontentnotification = 1; + + // Create the module. + update_module(clone $moduleinfo); // We use clone to keep the original object untouch for later use. + + // Redirect messages to sink and stop buffer output from CLI task. + $sink = $this->redirectMessages(); + ob_start(); + $this->runAdhocTasks('\core_course\task\content_notification_task'); + $output = ob_get_contents(); + ob_end_clean(); + $messages = $sink->get_messages(); + $sink->close(); + + // We have 3 students, one with a non-active enrolment that should not receive a notification. + $this->assertCount(2, $messages); + foreach ($messages as $message) { + $this->assertEquals('coursecontentupdated', $message->eventtype); + $this->assertEquals($modurl, $message->contexturl); + } + + // Now, set the course to not visible. + $DB->set_field('course', 'visible', 0, ['id' => $course->id]); + $this->setAdminUser(); + update_module(clone $moduleinfo); + + // Redirect messages to sink and stop buffer output from CLI task. + $sink = $this->redirectMessages(); + ob_start(); + $this->runAdhocTasks('\core_course\task\content_notification_task'); + $output = ob_get_contents(); + ob_end_clean(); + $messages = $sink->get_messages(); + $sink->close(); + // No messages, course not visible. + $this->assertCount(0, $messages); + + // Now, set the module to not visible. + $DB->set_field('course', 'visible', 1, ['id' => $course->id]); + $this->setAdminUser(); + $moduleinfo->visible = 0; + update_module(clone $moduleinfo); + + // Redirect messages to sink and stop buffer output from CLI task. + $sink = $this->redirectMessages(); + ob_start(); + $this->runAdhocTasks('\core_course\task\content_notification_task'); + $output = ob_get_contents(); + ob_end_clean(); + $messages = $sink->get_messages(); + $sink->close(); + // No messages, module not visible. + $this->assertCount(0, $messages); + } +} diff --git a/lang/en/course.php b/lang/en/course.php index 50649324ab7..5d5861e095f 100644 --- a/lang/en/course.php +++ b/lang/en/course.php @@ -59,6 +59,12 @@ $string['completion_setby:manual:done'] = '{$a->activityname} is marked by {$a-> $string['completion_setby:manual:markdone'] = '{$a->activityname} is marked by {$a->setby} as not done. Press to mark as done.'; $string['completionrequirements'] = 'Completion requirements for {$a}'; $string['coursealreadyfinished'] = 'Course already finished'; +$string['coursecontentnotification'] = 'Send content change notification'; +$string['coursecontentnotifnew'] = '{$a->coursename} new content'; +$string['coursecontentnotifnewbody'] = '

{$a->moduletypename} {$a->modulename} is new in the course {$a->coursename}.

Change your notification preferences

'; +$string['coursecontentnotifupdate'] = '{$a->coursename} content change'; +$string['coursecontentnotifupdatebody'] = '

{$a->moduletypename} {$a->modulename} has been changed in the course {$a->coursename}.

Change your notification preferences

'; +$string['coursecontentnotification_help'] = 'You can send a notification to the course users about any new or updated resource or activity. Please notice that the notification will be sent only if the new resource or activity is visible.'; $string['coursenotyetstarted'] = 'The course has not yet started'; $string['coursenotyetfinished'] = 'The course has not yet finished'; $string['coursetoolong'] = 'The course is too long';