Merge branch 'MDL-83871-main-v02' of https://github.com/ferranrecio/moodle

This commit is contained in:
Huong Nguyen
2025-02-03 09:37:25 +07:00
14 changed files with 499 additions and 17 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ Feature: Test tertiary navigation as various users
| user | createbutton | activetab |
| admin | should exist | More |
| teacher | should exist | More |
| nonediting | should not exist | Badges |
| nonediting | should not exist | More |
Scenario: Check navigation as a student
Given I am on the "C1" "Course" page logged in as "student1"
@@ -0,0 +1,192 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\output\local\overview;
use core\context\course as context_course;
use core\output\named_templatable;
use core\output\renderable;
use core\output\notification;
use core\plugin_manager;
use core\url;
use core_collator;
use stdClass;
/**
* Class overview page
*
* @package core_course
* @copyright 2025 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class overviewpage implements renderable, named_templatable {
/** @var context_course the context. */
protected context_course $context;
/**
* Constructor.
*
* @param stdClass $course the course object.
*/
public function __construct(
/** @var stdClass the course object */
protected stdClass $course,
) {
$this->context = context_course::instance($this->course->id);
}
#[\Override]
public function export_for_template(\renderer_base $output): stdClass {
$modfullnames = $this->get_course_activities_overview_list();
$elements = [];
foreach ($modfullnames as $modname => $modfullname) {
$elements[] = $this->export_activity_overview_section_data($output, $modname, $modfullname);
}
return (object) [
'elements' => $elements,
'courseid' => $this->course->id,
'contextid' => $this->context->id,
];
}
/**
* Retrieves a list of course activities overview.
*
* @return string[] An associative array module name => module plural name.
*/
private function get_course_activities_overview_list(): array {
$modinfo = get_fast_modinfo($this->course);
$modfullnames = [];
$archetypes = [];
foreach ($modinfo->cms as $cm) {
// Exclude activities that aren't visible or have no view link (e.g. label).
// Account for folder being displayed inline.
if (!$cm->uservisible || (!$cm->has_view() && strcmp($cm->modname, 'folder') !== 0)) {
continue;
}
if (array_key_exists($cm->modname, $modfullnames)) {
continue;
}
if (!array_key_exists($cm->modname, $archetypes)) {
$archetypes[$cm->modname] = plugin_supports(
type: 'mod',
name: $cm->modname,
feature: FEATURE_MOD_ARCHETYPE,
default: MOD_ARCHETYPE_OTHER
);
}
if ($archetypes[$cm->modname] == MOD_ARCHETYPE_RESOURCE) {
if (!array_key_exists('resource', $modfullnames)) {
$modfullnames['resource'] = get_string('resources');
}
} else {
$modfullnames[$cm->modname] = $cm->modplural;
}
}
core_collator::asort($modfullnames);
return $modfullnames;
}
/**
* Exports the data for the activity overview section.
*
* This function checks if the activity has an overview integration,
* and return the data accordingly.
*
* @param \renderer_base $output
* @param string $modname The name of the module.
* @param string $modfullname The full name of the module.
* @return stdClass The exported data for the activity overview section.
*/
private function export_activity_overview_section_data(
\renderer_base $output,
string $modname,
string $modfullname
): stdClass {
// For now the activity overview is only a links to the activity index.php file.
// In the next issue (MDL-83872) activities can implement their own overview integration.
return $this->export_legacy_overview($output, $modname, $modfullname);
}
/**
* Generates the activity overview icon for a given module.
*
* @param \renderer_base $output
* @param string $modname The name of the module for which the icon is being generated.
* @return string The HTML string for the activity overview icon.
*/
private function get_activity_overview_icon(\renderer_base $output, string $modname): string {
if ($modname === 'resource') {
return $output->pix_icon('monologo', '', 'mod_page', ['class' => 'icon iconsize-medium']);
}
return $output->pix_icon('monologo', '', "mod_$modname", ['class' => 'icon iconsize-medium']);
}
/**
* Exports the legacy overview for a given module.
*
* This export only applies to modules that do not have an overview integration.
*
* @param \renderer_base $output
* @param string $modname
* @param string $modfullname
* @return stdClass
*/
private function export_legacy_overview(
\renderer_base $output,
string $modname,
string $modfullname
): stdClass {
if ($modname === 'resource') {
$legacyoverview = 'resources.php';
$message = get_string('overview_missing_title', 'core_course', get_string('resource'));
} else {
$legacyoverview = '/mod/' . $modname . '/index.php';
$pluginman = plugin_manager::instance();
$message = get_string('overview_missing_title', 'core_course', $pluginman->plugin_name($modname));
}
$notification = new notification(
message: get_string('overview_missing_notice', 'core_course'),
messagetype: \core\notification::INFO,
closebutton: false,
title: $message,
titleicon: 'i/circleinfo',
);
return (object)[
'overviewurl' => new url($legacyoverview, ['id' => $this->course->id]),
'icon' => $this->get_activity_overview_icon($output, $modname),
'name' => $modfullname,
'shortname' => $modname,
'notification' => $notification->export_for_template($output),
];
}
/**
* Get the name of the template to use for this templatable.
*
* @param \renderer_base $renderer The renderer requesting the template name
* @return string
*/
public function get_template_name(\renderer_base $renderer): string {
return 'core_course/local/overview/overviewpage';
}
}
+6
View File
@@ -3682,6 +3682,7 @@ function course_get_user_navigation_options($context, $course = null) {
'search' => false,
'tags' => false,
'communication' => false,
'overview' => false,
];
$options->blogs = !empty($CFG->enableblogs) &&
@@ -3761,6 +3762,11 @@ function course_get_user_navigation_options($context, $course = null) {
$capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
$options->competencies = has_any_capability($capabilities, $context);
}
if ($isloggedin && !$isfrontpage) {
$options->overview = has_capability('moodle/course:viewoverview', $context);
}
return $options;
}
+54
View File
@@ -0,0 +1,54 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Course activities overview page.
*
* @package core_course
* @copyright 2025 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once('../config.php');
require_once('lib.php');
require_once($CFG->libdir . '/completionlib.php');
$courseid = required_param('id', PARAM_INT);
$PAGE->set_url('/course/overview.php', ['id' => $courseid]);
$course = get_course($courseid);
$context = context_course::instance($course->id, MUST_EXIST);
require_login($course);
require_capability('moodle/course:viewoverview', $context);
$output = $PAGE->get_renderer('format_' . $course->format);
$overview = new core_course\output\local\overview\overviewpage($course);
$PAGE->set_pagelayout('incourse');
$PAGE->set_title(get_string('overview_page_title', 'course', $course->fullname));
$PAGE->set_heading($course->fullname);
echo $output->header();
echo $output->heading(get_string('activities'), 2, 'h4');
echo $output->paragraph(get_string('overview_info', 'course'));
echo $output->render($overview);
echo $OUTPUT->footer();
@@ -0,0 +1,67 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template core_course/local/overview/overviewpage
Course activity overview page template.
Example context (json):
{
"elements": [
{
"name": "Activity 1",
"shortname": "forum",
"icon": "<i class='icon fa fa-graduation-cap'></i>",
"overviewurl": "https://moodle.org"
},
{
"name": "Activity 2",
"shortname": "activity2",
"icon": "<i class='icon fa fa-graduation-cap'></i>",
"overviewurl": "https://moodle.org"
}
],
"contextid": 1,
"courseid": 1
}
}}
<div id="course-overview-page">
{{#elements}}
{{<core/local/collapsable_section}}
{{$titlecontent}}{{{icon}}} {{name}}{{/titlecontent}}
{{$elementid}}{{shortname}}_overview{{/elementid}}
{{$extraclasses}}border border-1 rounded-3 p-3{{/extraclasses}}
{{$sectioncontent}}
<div class="d-flex flex-column justify-content-between">
{{#notification}}
{{>core/notification_info}}
{{/notification}}
<div>
<a
class="btn btn-outline-secondary"
href="{{overviewurl}}"
role="button"
>
{{#str}} goto_overview, course, {{name}} {{/str}}
<span class="ms-2">{{#pix}} i/arrow-right, core {{/pix}}</span>
</a>
</div>
</div>
{{/sectioncontent}}
{{/core/local/collapsable_section}}
{{/elements}}
</div>
+3
View File
@@ -205,6 +205,9 @@ class behat_course extends behat_base {
throw new Exception("The specified section $identifier does not exist.");
}
return new moodle_url('/course/' . $sectionpage, ['id' => $section->id]);
case 'activities':
$courseid = $this->get_course_id($identifier);
return new moodle_url('/course/overview.php', ['id' => $courseid]);
}
throw new Exception('Unrecognised core page type "' . $type . '."');
}
+130
View File
@@ -0,0 +1,130 @@
@core @core_course
Feature: Users can access the course activities overview page
In order to access the course activities overview page
As a user
I should be able to navigate to the course activities overview page
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | format | numsections | initsections |
| Course 1 | C1 | topics | 1 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activities" exist:
| activity | course | section | idnumber | name |
| assign | C1 | 1 | 1 | Test assignment name |
Scenario: Teacher can access the course overview page
Given I am on the "C1" "Course" page logged in as "teacher1"
When I follow "Activities"
Then I should see "Activities"
And I should see "Go to Assignments overview"
And I follow "Go to Assignments overview"
And I should see "Test assignment name"
And I should see "Needs grading: 0"
Scenario: Student can access the course overview page
Given I am on the "C1" "Course" page logged in as "student1"
When I follow "Activities"
Then I should see "Activities"
And I should see "Go to Assignments overview"
And I follow "Go to Assignments overview"
And I should see "Test assignment name"
And I should see "No submission"
Scenario: The activities overview shows only the type of activities present in the course
Given the following "activities" exist:
| activity | course | section | idnumber | name | visible |
| forum | C1 | 1 | 1 | Test forum name | 1 |
| choice | C1 | 1 | 2 | Test choice name | 0 |
# Teacher can see all activities.
When I am on the "Course 1" "course > activities" page logged in as "teacher1"
And I should see "Assignments" in the "region-main" "region"
And I should see "Forums" in the "region-main" "region"
And I should see "Choices" in the "region-main" "region"
And I should not see "Databases" in the "region-main" "region"
And I should not see "Feedback" in the "region-main" "region"
And I should not see "Glossaries" in the "region-main" "region"
And I should not see "Lessons" in the "region-main" "region"
And I should not see "External tools" in the "region-main" "region"
And I should not see "Quizzes" in the "region-main" "region"
And I should not see "SCORM packages" in the "region-main" "region"
And I should not see "Wikis" in the "region-main" "region"
And I should not see "Workshops" in the "region-main" "region"
And I should not see "Resources" in the "region-main" "region"
# Student can see only visible activities.
And I am on the "Course 1" "course > activities" page logged in as "student1"
And I should see "Assignments" in the "region-main" "region"
And I should see "Forums" in the "region-main" "region"
And I should not see "Choices" in the "region-main" "region"
And I should not see "Databases" in the "region-main" "region"
And I should not see "Feedback" in the "region-main" "region"
And I should not see "Glossaries" in the "region-main" "region"
And I should not see "Lessons" in the "region-main" "region"
And I should not see "External tools" in the "region-main" "region"
And I should not see "Quizzes" in the "region-main" "region"
And I should not see "SCORM packages" in the "region-main" "region"
And I should not see "Wikis" in the "region-main" "region"
And I should not see "Workshops" in the "region-main" "region"
And I should not see "Resources" in the "region-main" "region"
@javascript
Scenario: Teacher can expand and collapse course overview items
# Create another activity to test each activity type could be expanded independently.
Given the following "activities" exist:
| activity | course | section | idnumber | name |
| forum | C1 | 1 | 1 | Test forum name |
And I am on the "Course 1" "course > activities" page logged in as "teacher1"
And I should see "Assignments" in the "assign_overview_collapsible" "region"
And I should see "Forums" in the "forum_overview_collapsible" "region"
And I should not see "Go to Assignments overview" in the "assign_overview_collapsible" "region"
And I should not see "Go to Forums overview" in the "forum_overview_collapsible" "region"
When I click on "Expand" "link" in the "assign_overview_collapsible" "region"
Then I should see "Go to Assignments overview" in the "assign_overview_collapsible" "region"
And I should not see "Go to Forums overview" in the "forum_overview_collapsible" "region"
And I click on "Collapse" "link" in the "assign_overview_collapsible" "region"
And I should not see "Go to Assignments overview" in the "assign_overview_collapsible" "region"
And I should not see "Go to Forums overview" in the "forum_overview_collapsible" "region"
Scenario: Course overview shows the course present activity types
Given the following "activities" exist:
| activity | course | name |
| book | C1 | Activity 2 |
| choice | C1 | Activity 3 |
| data | C1 | Activity 4 |
| feedback | C1 | Activity 5 |
| folder | C1 | Activity 6 |
| forum | C1 | Activity 7 |
| glossary | C1 | Activity 8 |
| imscp | C1 | Activity 10 |
| label | C1 | Activity 11 |
| lesson | C1 | Activity 12 |
| lti | C1 | Activity 13 |
| page | C1 | Activity 14 |
| quiz | C1 | Activity 15 |
| resource | C1 | Activity 16 |
| scorm | C1 | Activity 17 |
| url | C1 | Activity 18 |
| wiki | C1 | Activity 19 |
| workshop | C1 | Activity 20 |
Given I am on the "Course 1" "course > activities" page logged in as "teacher1"
And I should see "Assignments" in the "assign_overview_collapsible" "region"
And I should see "Choices" in the "choice_overview_collapsible" "region"
And I should see "Databases" in the "data_overview_collapsible" "region"
And I should see "Feedback" in the "feedback_overview_collapsible" "region"
And I should see "Forums" in the "forum_overview_collapsible" "region"
And I should see "Glossaries" in the "glossary_overview_collapsible" "region"
And I should see "Lessons" in the "lesson_overview_collapsible" "region"
And I should see "External tools" in the "lti_overview_collapsible" "region"
And I should see "Quizzes" in the "quiz_overview_collapsible" "region"
And I should see "SCORM packages" in the "scorm_overview_collapsible" "region"
And I should see "Wikis" in the "wiki_overview_collapsible" "region"
And I should see "Workshops" in the "workshop_overview_collapsible" "region"
# All resources are grouped.
And I should see "Resources" in the "resource_overview_collapsible" "region"
+3 -1
View File
@@ -2847,7 +2847,7 @@ final class externallib_test extends externallib_advanced_testcase {
foreach ($course['options'] as $option) {
$navoptions->{$option['name']} = $option['available'];
}
$this->assertCount(9, $course['options']);
$this->assertCount(10, $course['options']);
if ($course['id'] == SITEID) {
$this->assertTrue($navoptions->blogs);
$this->assertFalse($navoptions->notes);
@@ -2858,6 +2858,7 @@ final class externallib_test extends externallib_advanced_testcase {
$this->assertFalse($navoptions->search);
$this->assertTrue($navoptions->competencies);
$this->assertFalse($navoptions->communication);
$this->assertFalse($navoptions->overview);
} else {
$this->assertTrue($navoptions->blogs);
$this->assertFalse($navoptions->notes);
@@ -2868,6 +2869,7 @@ final class externallib_test extends externallib_advanced_testcase {
$this->assertFalse($navoptions->search);
$this->assertTrue($navoptions->competencies);
$this->assertFalse($navoptions->communication);
$this->assertTrue($navoptions->overview);
}
}
}
+5
View File
@@ -100,6 +100,7 @@ $string['filterbothactive'] = 'First ({$a->first}) Last ({$a->last})';
$string['filterbyname'] = 'Filter by name';
$string['filterfirstactive'] = 'First ({$a->first})';
$string['filterlastactive'] = 'Last ({$a->last})';
$string['goto_overview'] = 'Go to {$a} overview';
$string['gradetopassnotset'] = 'This course does not have a grade to pass set. It may be set in the grade item of the course (Gradebook setup).';
$string['hideendedcoursestask'] = 'Hide courses on end date';
$string['informationformodule'] = 'Information about the {$a} activity';
@@ -115,6 +116,10 @@ $string['norecentaccessesinfomessage'] = 'Hi {$a->userfirstname},
<p>A number of students in {$a->coursename} have not accessed the course recently.</p>';
$string['noteachinginfomessage'] = 'Hi {$a->userfirstname},
<p>Courses with start dates in the next week have been identified as having no teacher or student enrolments.</p>';
$string['overview_info'] = 'View all the activities in this course, including key details like due date and updates.';
$string['overview_missing_notice'] = 'Check the overview page for more details.';
$string['overview_missing_title'] = 'Information not available here for {$a} activities';
$string['overview_page_title'] = 'Course activities: {$a}';
$string['participants:perpage'] = 'Number of participants per page';
$string['participants:perpage_help'] = 'The number of users shown per page on the participants page in each course.';
$string['participantsnavigation'] = 'Participants tertiary navigation.';
+1
View File
@@ -219,6 +219,7 @@ $string['course:viewhiddencourses'] = 'View hidden courses';
$string['course:viewhiddengroups'] = 'View hidden groups';
$string['course:viewhiddensections'] = 'View hidden sections';
$string['course:viewhiddenuserfields'] = 'View hidden user fields';
$string['course:viewoverview'] = 'View course activities overview page';
$string['course:viewparticipants'] = 'View participants';
$string['course:viewscales'] = 'View scales';
$string['course:visibility'] = 'Hide/show courses';
+15 -14
View File
@@ -57,8 +57,8 @@ class secondary extends view {
$nodes = [];
$nodes['settings'] = [
self::TYPE_CONTAINER => [
'coursereports' => 3,
'questionbank' => 4,
'coursereports' => 4,
'questionbank' => 5,
],
self::TYPE_SETTING => [
'editsettings' => 0,
@@ -71,28 +71,29 @@ class secondary extends view {
'otherusers' => 1.7,
'gradebooksetup' => 2.1,
'outcomes' => 2.2,
'coursecompletion' => 6,
'coursebadges' => 7.1,
'newbadge' => 7.2,
'filtermanagement' => 9,
'unenrolself' => 10,
'coursetags' => 11,
'download' => 12,
'contextlocking' => 13,
'coursecompletion' => 7,
'coursebadges' => 8.1,
'newbadge' => 8.2,
'filtermanagement' => 10,
'unenrolself' => 11,
'coursetags' => 12,
'download' => 13,
'contextlocking' => 14,
],
];
$nodes['navigation'] = [
self::TYPE_CONTAINER => [
'participants' => 1,
'courseoverview' => 3,
],
self::TYPE_SETTING => [
'grades' => 2,
'badgesview' => 7,
'competencies' => 8,
'communication' => 14,
'badgesview' => 8,
'competencies' => 9,
'communication' => 15,
],
self::TYPE_CUSTOM => [
'contentbank' => 5,
'contentbank' => 6,
'participants' => 1, // In site home, 'participants' is classified differently.
],
];
+11
View File
@@ -1025,6 +1025,17 @@ $capabilities = array(
)
),
'moodle/course:viewoverview' => [
'captype' => 'read',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => [
'student' => CAP_ALLOW,
'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW,
],
],
'moodle/course:changefullname' => array(
'riskbitmask' => RISK_XSS,
+10
View File
@@ -3162,6 +3162,16 @@ class global_navigation extends navigation_node {
navigation_node::TYPE_SETTING, null, 'communication');
}
if ($navoptions->overview) {
$coursenode->add(
text: get_string('activities'),
action: new moodle_url('/course/overview.php', ['id' => $course->id]),
type: self::TYPE_CONTAINER,
key: 'courseoverview',
icon: new pix_icon('i/info', ''),
);
}
return true;
}
/**
+1 -1
View File
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2025013100.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2025013100.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '5.0dev (Build: 20250131)'; // Human-friendly version name