From 4f774171ee458e02af937d772d1697472a731e96 Mon Sep 17 00:00:00 2001 From: Amaia Anabitarte Date: Tue, 10 Feb 2026 13:06:34 +0100 Subject: [PATCH] MDL-87679 core_course: New event for viewing restricted elements --- .upgradenotes/MDL-87679-2026021011315509.yml | 8 +++ .../tests/behat/display_availability.feature | 9 +++ .../route/controller/restricted_module.php | 8 +++ .../route/controller/restricted_section.php | 6 +- public/course/lib.php | 9 ++- public/course/tests/courselib_test.php | 31 +++++++++ public/lang/en/moodle.php | 2 + .../event/course_restricted_module_viewed.php | 67 +++++++++++++++++++ .../event/restricted_section_viewed.php | 44 ++++++++++++ .../course_restricted_module_viewed_test.php | 63 +++++++++++++++++ 10 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 .upgradenotes/MDL-87679-2026021011315509.yml create mode 100644 public/lib/classes/event/course_restricted_module_viewed.php create mode 100644 public/lib/classes/event/restricted_section_viewed.php create mode 100644 public/lib/tests/event/course_restricted_module_viewed_test.php diff --git a/.upgradenotes/MDL-87679-2026021011315509.yml b/.upgradenotes/MDL-87679-2026021011315509.yml new file mode 100644 index 00000000000..aa50005303f --- /dev/null +++ b/.upgradenotes/MDL-87679-2026021011315509.yml @@ -0,0 +1,8 @@ +issueNumber: MDL-87679 +notes: + core_course: + - message: >- + New 'restricted' parameter has been added to course_section_view() + function to define whether the section was restricted for the user or + not. + type: improved diff --git a/public/availability/tests/behat/display_availability.feature b/public/availability/tests/behat/display_availability.feature index c873cda4426..ff49b90accd 100644 --- a/public/availability/tests/behat/display_availability.feature +++ b/public/availability/tests/behat/display_availability.feature @@ -208,3 +208,12 @@ Feature: Display availability for activities and sections And I should see "Section 1" in the "page-header" "region" And I should see "Not available unless" in the "region-main" "region" And "Page 1" "link" should not exist in the "region-main" "region" + # Check the logs. + And I am on the "C1" "Course" page logged in as "teacher1" + And I navigate to "Reports > Logs" in current page administration + And I click on "Get these logs" "button" + And I should see "Restricted section viewed" + And I click on "Restricted section viewed" "link" + And I switch to "action" window + And I should see "Section 1" in the "page-header" "region" + And I should see "Not available unless" in the "region-main" "region" diff --git a/public/course/classes/route/controller/restricted_module.php b/public/course/classes/route/controller/restricted_module.php index 02673903f4d..bd36eb4838b 100644 --- a/public/course/classes/route/controller/restricted_module.php +++ b/public/course/classes/route/controller/restricted_module.php @@ -93,6 +93,14 @@ class restricted_module { $response->getBody()->write($renderer->render($cmoutput)); $response->getBody()->write($OUTPUT->footer()); + $eventdata = [ + 'objectid' => $cm->id, + 'context' => $context, + ]; + $event = \core\event\course_restricted_module_viewed::create($eventdata); + $event->trigger(); + user_accesstime_log($context->instanceid); + return $response; } } diff --git a/public/course/classes/route/controller/restricted_section.php b/public/course/classes/route/controller/restricted_section.php index 533aef54485..82db465ae28 100644 --- a/public/course/classes/route/controller/restricted_section.php +++ b/public/course/classes/route/controller/restricted_section.php @@ -60,6 +60,7 @@ class restricted_section { require_once($CFG->dirroot . '/course/lib.php'); $course = get_course($section->course); + $context = \context_course::instance($course->id); $format = course_get_format($course->id); $format->set_sectionid($section->id); $outputclass = $format->get_output_classname('content'); @@ -70,7 +71,7 @@ class restricted_section { $PAGE->set_pagelayout('course'); $PAGE->add_body_classes(['limitedwidth', 'single-section-page']); $PAGE->set_pagetype('course-view-section-' . $course->format . '-restricted'); - $PAGE->set_context(\context_course::instance($course->id)); + $PAGE->set_context($context); $sectiontitle = $format->get_section_name($section); $strtitle = get_string('restrictedtitle', 'course', $sectiontitle); @@ -82,6 +83,9 @@ class restricted_section { $response->getBody()->write($renderer->render($sectionoutput)); $response->getBody()->write($OUTPUT->footer()); + // Trigger section viewed event. + course_section_view($context, $section->id, true); + return $response; } } diff --git a/public/course/lib.php b/public/course/lib.php index bbb074e9009..e16c58a38d6 100644 --- a/public/course/lib.php +++ b/public/course/lib.php @@ -4255,15 +4255,20 @@ function course_update_communication_instance_data(stdClass $data): void { * * @param context_course $context course context object * @param int $sectionid section number + * @param bool $restricted Whether the section is restricted for the user or not. * @since Moodle 4.4. */ -function course_section_view(context_course $context, int $sectionid): void { +function course_section_view(context_course $context, int $sectionid, bool $restricted = false): void { $eventdata = [ 'objectid' => $sectionid, 'context' => $context, ]; - $event = \core\event\section_viewed::create($eventdata); + if ($restricted) { + $event = \core\event\restricted_section_viewed::create($eventdata); + } else { + $event = \core\event\section_viewed::create($eventdata); + } $event->trigger(); user_accesstime_log($context->instanceid); diff --git a/public/course/tests/courselib_test.php b/public/course/tests/courselib_test.php index 4a9e67f5b42..3a33ed7a935 100644 --- a/public/course/tests/courselib_test.php +++ b/public/course/tests/courselib_test.php @@ -7234,6 +7234,37 @@ final class courselib_test extends advanced_testcase { $this->assertEquals($section->id, $event->objectid); } + /** + * Test course_section_view() function for restricted sections. + * + * @covers ::course_section_view + */ + public function test_course_restricted_section_view(): void { + + $this->resetAfterTest(); + + // Course without sections. + $course = $this->getDataGenerator()->create_course(['numsections' => 5], ['createsections' => true]); + $coursecontext = context_course::instance($course->id); + $format = course_get_format($course->id); + $sections = $format->get_sections(); + $section = reset($sections); + + // Redirect events to the sink, so we can recover them later. + $sink = $this->redirectEvents(); + + course_section_view($coursecontext, $section->id, true); + + $events = $sink->get_events(); + $event = reset($events); + + // Check the event details are correct. + $this->assertInstanceOf('\core\event\restricted_section_viewed', $event); + $this->assertEquals(context_course::instance($course->id), $event->get_context()); + $this->assertEquals('course_sections', $event->objecttable); + $this->assertEquals($section->id, $event->objectid); + } + /** * Tests get_sorted_course_formats returns plugins in cases where plugins are * installed previously but no longer exist, or not installed yet. diff --git a/public/lang/en/moodle.php b/public/lang/en/moodle.php index 5d2710a1a45..35e7fc9bacd 100644 --- a/public/lang/en/moodle.php +++ b/public/lang/en/moodle.php @@ -826,6 +826,7 @@ $string['eventcoursessearched'] = 'Courses searched'; $string['eventcourseresetended'] = 'Course reset ended'; $string['eventcourseresetstarted'] = 'Course reset started'; $string['eventcourserestored'] = 'Course restored'; +$string['eventcourserestrictedmoduleviewed'] = 'Restricted module viewed'; $string['eventcoursestarted'] = 'Course started'; $string['eventcourseupdated'] = 'Course updated'; $string['eventcoursesectioncreated'] = 'Course section created'; @@ -842,6 +843,7 @@ $string['eventemailfailed'] = 'Email failed to send'; $string['eventmycoursesviewed'] = 'My courses viewed'; $string['eventname'] = 'Event name'; $string['eventrecentactivityviewed'] = 'Recent activity viewed'; +$string['eventrestrictedsectionviewed'] = 'Restricted section viewed'; $string['eventsearchindexed'] = 'Search data indexed'; $string['eventsearchresultsviewed'] = 'Search results viewed'; $string['eventsectionviewed'] = 'Section viewed'; diff --git a/public/lib/classes/event/course_restricted_module_viewed.php b/public/lib/classes/event/course_restricted_module_viewed.php new file mode 100644 index 00000000000..e5333c33c44 --- /dev/null +++ b/public/lib/classes/event/course_restricted_module_viewed.php @@ -0,0 +1,67 @@ +. + +namespace core\event; + +/** + * Restricted module viewed event class. + * + * @package core + * @copyright 2026 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_restricted_module_viewed extends \core\event\course_module_viewed { + /** + * Init method. + * + * @return void + */ + protected function init() { + $this->data['objecttable'] = 'course_modules'; + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Returns description of what happened. + * + * @return string + */ + public function get_description() { + $cm = get_coursemodule_from_id(null, $this->contextinstanceid, 0, false, MUST_EXIST); + return "The user with id '$this->userid' viewed the restricted '{$cm->modname}' activity with " . + "course module id '$this->contextinstanceid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventcourserestrictedmoduleviewed', 'core'); + } + + /** + * Get URL related to the action. + * + * @return \moodle_url + */ + public function get_url() { + $cm = get_coursemodule_from_id(null, $this->contextinstanceid, 0, false, MUST_EXIST); + return new \moodle_url("/mod/$cm->modname/view.php", ['id' => $this->contextinstanceid]); + } +} diff --git a/public/lib/classes/event/restricted_section_viewed.php b/public/lib/classes/event/restricted_section_viewed.php new file mode 100644 index 00000000000..86d130569cb --- /dev/null +++ b/public/lib/classes/event/restricted_section_viewed.php @@ -0,0 +1,44 @@ +. + +namespace core\event; + +/** + * Restricted section viewed event class. + * + * @package core + * @copyright 2026 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class restricted_section_viewed extends section_viewed { + /** + * Returns non-localised description of what happened. + * + * @return string + */ + public function get_description() { + return "The user with id '$this->userid' viewed the restricted section with id '$this->objectid'."; + } + + /** + * Return localised event name. + * + * @return string + */ + public static function get_name() { + return get_string('eventrestrictedsectionviewed', 'core'); + } +} diff --git a/public/lib/tests/event/course_restricted_module_viewed_test.php b/public/lib/tests/event/course_restricted_module_viewed_test.php new file mode 100644 index 00000000000..359eb704cd3 --- /dev/null +++ b/public/lib/tests/event/course_restricted_module_viewed_test.php @@ -0,0 +1,63 @@ +. + +namespace core\event; + +use advanced_testcase; +use context_module; +use stdClass; +use moodle_url; + +/** + * Tests for base course module viewed event. + * + * @package core + * @covers \core\event\course_restricted_module_viewed + * @copyright 2026 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class course_restricted_module_viewed_test extends advanced_testcase { + /** + * Test event properties and methods. + */ + public function test_event_attributes(): void { + + $this->resetAfterTest(); + $course = $this->getDataGenerator()->create_course(); + $record = new stdClass(); + $record->course = $course->id; + $feed = $this->getDataGenerator()->create_module('feedback', $record); + $cm = get_coursemodule_from_instance('feedback', $feed->id); + $context = context_module::instance($cm->id); + + // Trigger the page view event. + $sink = $this->redirectEvents(); + $pageevent = course_restricted_module_viewed::create([ + 'context' => $context, + 'courseid' => $course->id, + 'objectid' => $cm->id, + ]); + $pageevent->trigger(); + $result = $sink->get_events(); + $event = reset($result); + $sink->close(); + + $this->assertSame('course_modules', $event->objecttable); + $url = new moodle_url('/mod/feedback/view.php', ['id' => $cm->id]); + $this->assertEquals($url, $event->get_url()); + $this->assertEventContextNotUsed($event); + } +}