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

This commit is contained in:
Sara Arjona 2025-02-25 12:18:38 +01:00
commit 741c072b58
No known key found for this signature in database
22 changed files with 795 additions and 233 deletions

View File

@ -0,0 +1,9 @@
issueNumber: MDL-83892
notes:
core:
- message: >-
A new method get_instance_record has been added to cm_info object so
core can get the activity table record without using the $DB object
every time. Also, the method caches de result so getting more than once
per execution is much faster.
type: improved

View File

@ -21,6 +21,7 @@ use core\context\module as module_context;
use core_completion\cm_completion_details;
use core_courseformat\local\overview\overviewitem;
use core_courseformat\output\local\overview\activityname;
use core_courseformat\output\local\overview\overviewpage;
use core_courseformat\base as courseformat;
/**
@ -69,6 +70,16 @@ abstract class activityoverviewbase {
$this->format = courseformat::instance($this->course);
}
/**
* Redirects to the overview page for the activity.
*
* @param int $courseid The course id.
* @param string $modname The module name.
*/
public static function redirect_to_overview_page(int $courseid, string $modname): void {
redirect(overviewpage::get_modname_url($courseid, $modname));
}
/**
* Get the plugin specific overview items for the activity.
*

View File

@ -53,13 +53,17 @@ class activityname implements renderable, named_templatable {
$section = $this->cm->get_section_info();
$course = $this->cm->get_course();
$format = course_format::instance($course);
return (object) [
$result = (object) [
'activityname' => \core_external\util::format_string($cm->name, $cm->context, true),
'activityurl' => $cm->url,
'sectiontitle' => $format->get_section_name($section),
'hidden' => empty($cm->visible),
'stealth' => $cm->is_stealth(),
];
if ($format->uses_sections()) {
$result->sectiontitle = $format->get_section_name($section);
}
return $result;
}
/**

View File

@ -51,6 +51,21 @@ class overviewpage implements renderable, named_templatable {
$this->context = context_course::instance($this->course->id);
}
/**
* Gets the URL to the course overview page for a given course and module name.
*
* @param int $courseid
* @param string $modname
* @return url
*/
public static function get_modname_url(int $courseid, string $modname): url {
return new url(
url: '/course/overview.php',
params: ['id' => $courseid, 'expand[]' => $modname],
anchor: "{$modname}_overview_collapsible",
);
}
#[\Override]
public function export_for_template(\renderer_base $output): stdClass {
$modfullnames = $this->get_course_activities_overview_list();

View File

@ -32,15 +32,23 @@
<div class="fw-bold">
<a href="{{activityurl}}" class="activityname">{{activityname}}</a>
</div>
{{#sectiontitle}}
<div>
{{sectiontitle}}
</div>
{{/sectiontitle}}
<div class="ml-2">
{{#hidden}}
<span class="badge badge-success">{{#str}}hiddenfromstudents{{/str}}</span>
<span class="badge rounded-pill text-bg-secondary fw-normal">
{{#pix}}i/show, core{{/pix}}
{{#str}}hiddenfromstudents{{/str}}
</span>
{{/hidden}}
{{#stealth}}
<span class="badge badge-warning">{{#str}}hiddenoncoursepage{{/str}}</span>
<span class="badge rounded-pill text-bg-secondary fw-normal">
{{#pix}}t/stealth, core{{/pix}}
{{#str}}hiddenoncoursepage{{/str}}
</span>
{{/stealth}}
</div>
</div>

View File

@ -72,7 +72,7 @@
]
}
}}
<div class="course-overview border border-secondary border-1 rounded">
<div class="course-overview border border-secondary border-1 rounded pt-2">
<table
class="course-overview-table boxaligncenter {{!
}} table table-responsive w-100 d-block d-md-table"

View File

@ -4832,8 +4832,32 @@ function course_output_fragment_course_overview($args) {
}
$modname = $args['modname'];
$course = get_course($args['courseid']);
$context = context_course::instance($course->id, MUST_EXIST);
can_access_course($course);
// Some plugins may have a list view event.
$eventclassname = 'mod_' . $modname . '\\event\\course_module_instance_list_viewed';
// Do not confuse this "resource" with the "mod_resource" module.
// This "resource" is the table that aggregate all activities considered "resources"
// (files, folders, pages, text and media...). While the "mod_resource" is a poorly
// named plugin representing an uploaded file, and it is also one of the activities
// that can be aggregated in the "resource" table.
if ($modname === 'resource') {
$eventclassname = 'core\\event\\course_resources_list_viewed';
}
if (class_exists($eventclassname)) {
try {
$event = $eventclassname::create(['context' => $context]);
$event->add_record_snapshot('course', $course);
$event->trigger();
} catch (\Throwable $th) {
// This may happens if the plugin implements a custom event class.
// It is highly unlikely but we should not stop the rendering because of this.
// Instead, we will log the error and continue.
debugging('Error while triggering the course module instance viewed event: ' . $th->getMessage());
}
}
$content = '';
$format = course_get_format($course);
$renderer = $format->get_renderer($PAGE);

View File

@ -40,6 +40,11 @@ $context = context_course::instance($course->id, MUST_EXIST);
require_login($course);
require_capability('moodle/course:viewoverview', $context);
// Trigger event, course information viewed.
$event = \core\event\course_overview_viewed::create(['context' => $context]);
$event->add_record_snapshot('course', $course);
$event->trigger();
$format = course_get_format($course);
$renderer = $format->get_renderer($PAGE);
$overviewpageclass = $format->get_output_classname('overview\\overviewpage');

View File

@ -24,127 +24,7 @@
*/
require_once('../config.php');
require_once("$CFG->libdir/resourcelib.php");
$id = required_param('id', PARAM_INT); // course id
$courseid = required_param('id', PARAM_INT);
$course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST);
$PAGE->set_pagelayout('incourse');
require_course_login($course, true);
// get list of all resource-like modules
$allmodules = $DB->get_records('modules', array('visible'=>1));
$availableresources = array();
foreach ($allmodules as $key=>$module) {
$modname = $module->name;
$libfile = "$CFG->dirroot/mod/$modname/lib.php";
if (!file_exists($libfile)) {
continue;
}
$archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
if ($archetype != MOD_ARCHETYPE_RESOURCE) {
continue;
}
$availableresources[] = $modname;
}
// Triger view event.
$event = \core\event\course_resources_list_viewed::create(array('context' => context_course::instance($course->id)));
$event->add_record_snapshot('course', $course);
$event->trigger();
$strresources = get_string('resources');
$strname = get_string('name');
$strintro = get_string('moduleintro');
$strlastmodified = get_string('lastmodified');
$PAGE->set_url('/course/resources.php', array('id' => $course->id));
$PAGE->set_title($course->shortname.': '.$strresources);
$PAGE->set_heading($course->fullname);
$PAGE->navbar->add($strresources);
echo $OUTPUT->header();
$modinfo = get_fast_modinfo($course);
$usesections = course_format_uses_sections($course->format);
$cms = array();
$resources = array();
foreach ($modinfo->cms as $cm) {
if (!in_array($cm->modname, $availableresources)) {
continue;
}
// 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;
}
$cms[$cm->id] = $cm;
$resources[$cm->modname][] = $cm->instance;
}
// preload instances
foreach ($resources as $modname=>$instances) {
$additionalfields = '';
if (plugin_supports('mod', $modname, FEATURE_MOD_INTRO)) {
$additionalfields = ',intro,introformat';
}
$resources[$modname] = $DB->get_records_list($modname, 'id', $instances, 'id', 'id,name'.$additionalfields);
}
if (!$cms) {
notice(get_string('thereareno', 'moodle', $strresources), "$CFG->wwwroot/course/view.php?id=$course->id");
exit;
}
$table = new html_table();
$table->attributes['class'] = 'generaltable mod_index';
if ($usesections) {
$strsectionname = course_get_format($course)->get_generic_section_name();
$table->head = array ($strsectionname, $strname, $strintro);
$table->align = array ('center', 'left', 'left');
} else {
$table->head = array ($strlastmodified, $strname, $strintro);
$table->align = array ('left', 'left', 'left');
}
$currentsection = '';
foreach ($cms as $cm) {
if (!isset($resources[$cm->modname][$cm->instance])) {
continue;
}
$resource = $resources[$cm->modname][$cm->instance];
$printsection = '';
if ($usesections) {
if ($cm->sectionnum !== $currentsection) {
if ($cm->sectionnum) {
$printsection = get_section_name($course, $cm->sectionnum);
}
if ($currentsection !== '') {
$table->data[] = 'hr';
}
$currentsection = $cm->sectionnum;
}
}
$extra = empty($cm->extra) ? '' : $cm->extra;
$icon = '<img src="'.$cm->get_icon_url().'" class="activityicon" alt="'.$cm->get_module_type_name().'" /> ';
if (isset($resource->intro) && isset($resource->introformat)) {
$intro = format_module_intro($cm->modname, $resource, $cm->id);
} else {
$intro = '';
}
$class = $cm->visible ? '' : 'class="dimmed"'; // hidden modules are dimmed
$url = $cm->url ?: new moodle_url("/mod/{$cm->modname}/view.php", ['id' => $cm->id]);
$table->data[] = array (
$printsection,
"<a $class $extra href=\"" . $url ."\">" . $icon . $cm->get_formatted_name() . "</a>",
$intro);
}
echo html_writer::table($table);
echo $OUTPUT->footer();
\core_courseformat\activityoverviewbase::redirect_to_overview_page($courseid, 'resource');

View File

@ -215,3 +215,38 @@ Feature: Users can access the course activities overview page
When I am on the "Course 1" "course > activities > resource" page logged in as "student1"
Then I should see "To do" in the "Activity 1" "table_row"
And I should see "View" in the "Activity 1" "table_row"
Scenario: The course overview page should log a page event and a reource list event
Given the following "activity" exists:
| activity | folder |
| name | Activity 1 |
| course | C1 |
And I am on the "Course 1" "course > activities" page logged in as "teacher1"
And I am on the "Course 1" "course > activities > resource" page logged in as "student1"
When I am on the "Course 1" "course" page logged in as "teacher1"
And I navigate to "Reports" in current page administration
And I click on "Logs" "link"
Then I set the field "Select a user" to "Teacher 1"
And I click on "Get these logs" "button"
And I should see "Course activities overview page viewed"
And I should not see "viewed the list of resources"
And I set the field "Select a user" to "Student 1"
And I click on "Get these logs" "button"
And I should see "Course activities overview page viewed"
And I should see "viewed the list of resources"
@javascript
Scenario: The course overview page should log reource list event when loading the overview table
Given the following "activity" exists:
| activity | folder |
| name | Activity 1 |
| course | C1 |
And I am on the "Course 1" "course > activities" page logged in as "teacher1"
And I click on "Expand" "link" in the "resource_overview_collapsible" "region"
When I am on the "Course 1" "course" page logged in as "teacher1"
And I navigate to "Reports" in current page administration
And I click on "Logs" "link"
And I set the field "Select a user" to "Teacher 1"
And I click on "Get these logs" "button"
Then I should see "Course activities overview page viewed"
And I should see "viewed the list of resources"

View File

@ -137,4 +137,37 @@ final class events_test extends \advanced_testcase {
$sink->close();
}
/**
* Test the course activities overview page viewed.
*
* There is no external API for viewing course information so the unit test will simply
* create and trigger the event and ensure data is returned as expected.
*
* @covers \core\event\course_overview_viewed
*/
public function test_course_overview_viewed_event(): void {
// Create a course.
$data = new \stdClass();
$course = $this->getDataGenerator()->create_course($data);
$eventparams = [
'context' => \context_course::instance($course->id),
];
$event = \core\event\course_overview_viewed::create($eventparams);
// Trigger and capture the event.
$sink = $this->redirectEvents();
$event->trigger();
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\course_overview_viewed', $event);
$this->assertEquals($course->id, $event->courseid);
$this->assertDebuggingNotCalled();
$sink->close();
}
}

View File

@ -343,6 +343,7 @@ $string['copyrightnotice'] = 'Copyright notice';
$string['coresystem'] = 'System';
$string['cost'] = 'Cost';
$string['costdefault'] = 'Default cost';
$string['count_of_total'] = '<strong>{$a->count}</strong> of {$a->total}';
$string['counteditems'] = '{$a->count} {$a->items}';
$string['country'] = 'Country';
$string['course'] = 'Course';
@ -834,6 +835,7 @@ $string['eventcoursemodulecreated'] = 'Course module created';
$string['eventcoursemoduledeleted'] = 'Course module deleted';
$string['eventcoursemoduleupdated'] = 'Course module updated';
$string['eventcoursemoduleviewed'] = 'Course module viewed';
$string['eventcourseoverviewviewed'] = 'Course activities overview page viewed';
$string['eventcoursessearched'] = 'Courses searched';
$string['eventcourseresetended'] = 'Course reset ended';
$string['eventcourseresetstarted'] = 'Course reset started';

View File

@ -0,0 +1,77 @@
<?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\event;
use core\url;
/**
* Event course_overview_viewed
*
* @package core
* @copyright 2025 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_overview_viewed extends \core\event\base {
/**
* Set basic properties for the event.
*/
protected function init() {
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Returns description of what happened.
*
* @return string
*/
public function get_description(): string {
return "The user with id '$this->userid' viewed the course activity overview for the course with id '$this->courseid'.";
}
/**
* Return localised event name.
*
* @return string
*/
public static function get_name(): string {
return get_string('eventcourseoverviewviewed', 'core');
}
/**
* Get URL related to the action.
*
* @return url|null
*/
public function get_url() {
return new url('/course/overview.php', ['id' => $this->courseid]);
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if ($this->contextlevel != CONTEXT_COURSE) {
throw new \coding_exception('Context level must be CONTEXT_COURSE.');
}
}
}

View File

@ -1481,6 +1481,12 @@ class cm_info implements IteratorAggregate {
*/
private $iconcomponent;
/**
* The instance record form the module table
* @var stdClass
*/
private $instancerecord;
/**
* Name of module e.g. 'forum' (this is the same name as the module's main database
* table) - from cached data in modinfo field
@ -2216,6 +2222,25 @@ class cm_info implements IteratorAggregate {
return $cmrecord;
}
/**
* Return the activity database table record.
*
* The instance record will be cached after the first call.
*
* @return stdClass
*/
public function get_instance_record() {
global $DB;
if (!isset($this->instancerecord)) {
$this->instancerecord = $DB->get_record(
table: $this->modname,
conditions: ['id' => $this->instance],
strictness: MUST_EXIST,
);
}
return $this->instancerecord;
}
/**
* Returns the section delegated by this module, if any.
*

View File

@ -2240,4 +2240,34 @@ final class modinfolib_test extends advanced_testcase {
$cms = $sectioninfo->get_sequence_cm_infos();
$this->assertCount(0, $cms);
}
/**
* Test for cm_info::get_instance_record
*
* @covers \cm_info::get_instance_record
* @return void
*/
public function test_section_get_instance_record(): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['numsections' => 2]);
$activity = $this->getDataGenerator()->create_module('page', ['course' => $course], ['section' => 0]);
$modinfo = get_fast_modinfo($course->id);
$cminfo = $modinfo->get_cm($activity->cmid);
$instancerecord = $DB->get_record('page', ['id' => $activity->id]);
$instance = $cminfo->get_instance_record();
$this->assertEquals($instancerecord, $instance);
// The instance record should be cached.
$DB->delete_records('page', ['id' => $activity->id]);
$instance2 = $cminfo->get_instance_record();
$this->assertEquals($instancerecord, $instance);
$this->assertEquals($instance, $instance2);
}
}

View File

@ -0,0 +1,137 @@
<?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 mod_feedback\courseformat;
use core_courseformat\local\overview\overviewitem;
use core\output\action_link;
use core\output\local\properties\button;
use core\output\local\properties\text_align;
use core\url;
use core\output\pix_icon;
/**
* Class overview
*
* @package mod_feedback
* @copyright 2025 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class overview extends \core_courseformat\activityoverviewbase {
#[\Override]
public function get_extra_overview_items(): array {
return [
'submitted' => $this->get_extra_submitted_overview(),
];
}
#[\Override]
public function get_actions_overview(): ?overviewitem {
global $CFG, $USER;
if (!has_capability('mod/feedback:viewreports', $this->context)) {
return null;
}
require_once($CFG->dirroot . '/mod/feedback/lib.php');
$submissions = feedback_get_completeds_group_count(
$this->cm->get_instance_record()
);
// Normalize the value.
if (!$submissions) {
$submissions = 0;
}
$total = $submissions + feedback_count_incomplete_users($this->cm);
$content = new action_link(
url: new url('/mod/feedback/show_entries.php', ['id' => $this->cm->id]),
text: get_string(
'count_of_total',
'core',
['count' => $submissions, 'total' => $total]
),
attributes: ['class' => button::SECONDARY_OUTLINE->classes()],
);
return new overviewitem(
name: get_string('responses', 'mod_feedback'),
value: $submissions,
content: $content,
textalign: text_align::CENTER,
);
}
#[\Override]
public function get_due_date_overview(): ?overviewitem {
$duedate = null;
if (isset($this->cm->customdata['timeclose'])) {
$duedate = $this->cm->customdata['timeclose'];
}
if (empty($duedate)) {
return new overviewitem(
name: get_string('feedbackclose', 'mod_feedback'),
value: null,
content: '-',
);
}
return new overviewitem(
name: get_string('feedbackclose', 'mod_feedback'),
value: $duedate,
content: userdate($duedate),
);
}
/**
* Get the submitted status overview item.
*
* @return overviewitem|null The overview item (or null if the user cannot complete the feedback).
*/
private function get_extra_submitted_overview(): ?overviewitem {
global $USER;
if (!has_capability('mod/feedback:complete', $this->context)) {
return null;
}
$structure = new \mod_feedback_structure(
feedback: $this->cm->get_instance_record(),
cm: $this->cm,
courseid: $this->course->id,
userid: $USER->id,
);
$value = false;
$content = '-';
if ($structure->is_already_submitted()) {
$value = true;
$content = new pix_icon(
'i/checkedcircle',
alt: get_string('this_feedback_is_already_submitted', 'mod_feedback'),
attributes: ['class' => 'text-success'],
);
}
return new overviewitem(
name: get_string('responded', 'mod_feedback'),
value: $value,
content: $content,
textalign: text_align::CENTER,
);
}
}

View File

@ -134,7 +134,11 @@ if ($feedbackcompletion->is_empty()) {
}
} else {
echo $OUTPUT->box_start('generalbox boxaligncenter');
echo $OUTPUT->notification(get_string('this_feedback_is_already_submitted', 'feedback'));
echo $OUTPUT->notification(
get_string('this_feedback_is_already_submitted', 'feedback'),
\core\output\notification::NOTIFY_INFO,
closebutton: false,
);
echo $OUTPUT->continue_button(course_get_url($courseid ?: $course->id));
echo $OUTPUT->box_end();
}

View File

@ -23,107 +23,7 @@
*/
require_once("../../config.php");
require_once("lib.php");
$id = required_param('id', PARAM_INT);
$courseid = required_param('id', PARAM_INT);
$url = new moodle_url('/mod/feedback/index.php', array('id'=>$id));
$PAGE->set_url($url);
if (!$course = $DB->get_record('course', array('id'=>$id))) {
throw new \moodle_exception('invalidcourseid');
}
$context = context_course::instance($course->id);
require_login($course);
$PAGE->set_pagelayout('incourse');
$PAGE->add_body_class('limitedwidth');
// Trigger instances list viewed event.
$event = \mod_feedback\event\course_module_instance_list_viewed::create(array('context' => $context));
$event->add_record_snapshot('course', $course);
$event->trigger();
/// Print the page header
$strfeedbacks = get_string("modulenameplural", "feedback");
$strfeedback = get_string("modulename", "feedback");
$PAGE->navbar->add($strfeedbacks);
$PAGE->set_heading($course->fullname);
$PAGE->set_title(get_string('modulename', 'feedback').' '.get_string('activities'));
echo $OUTPUT->header();
if (!$PAGE->has_secondary_navigation()) {
echo $OUTPUT->heading($strfeedbacks);
}
/// Get all the appropriate data
if (! $feedbacks = get_all_instances_in_course("feedback", $course)) {
$url = new moodle_url('/course/view.php', array('id'=>$course->id));
notice(get_string('thereareno', 'moodle', $strfeedbacks), $url);
die;
}
$usesections = course_format_uses_sections($course->format);
/// Print the list of instances (your module will probably extend this)
$timenow = time();
$strname = get_string("name");
$strresponses = get_string('responses', 'feedback');
$table = new html_table();
if ($usesections) {
$strsectionname = course_get_format($course)->get_generic_section_name();
if (has_capability('mod/feedback:viewreports', $context)) {
$table->head = array ($strsectionname, $strname, $strresponses);
$table->align = array ("center", "left", 'center');
} else {
$table->head = array ($strsectionname, $strname);
$table->align = array ("center", "left");
}
} else {
if (has_capability('mod/feedback:viewreports', $context)) {
$table->head = array ($strname, $strresponses);
$table->align = array ("left", "center");
} else {
$table->head = array ($strname);
$table->align = array ("left");
}
}
foreach ($feedbacks as $feedback) {
//get the responses of each feedback
$viewurl = new moodle_url('/mod/feedback/view.php', array('id'=>$feedback->coursemodule));
if (has_capability('mod/feedback:viewreports', $context)) {
$completed_feedback_count = intval(feedback_get_completeds_group_count($feedback));
}
$dimmedclass = $feedback->visible ? '' : 'class="dimmed"';
$link = '<a '.$dimmedclass.' href="'.$viewurl->out().'">'.$feedback->name.'</a>';
if ($usesections) {
$tabledata = array (get_section_name($course, $feedback->section), $link);
} else {
$tabledata = array ($link);
}
if (has_capability('mod/feedback:viewreports', $context)) {
$tabledata[] = $completed_feedback_count;
}
$table->data[] = $tabledata;
}
echo "<br />";
echo html_writer::table($table);
/// Finish the page
echo $OUTPUT->footer();
\core_courseformat\activityoverviewbase::redirect_to_overview_page($courseid, 'feedback');

View File

@ -114,7 +114,7 @@ $string['eventresponsedeleted'] = 'Response deleted';
$string['eventresponsesubmitted'] = 'Response submitted';
$string['feedbackcompleted'] = '{$a->username} completed {$a->feedbackname}';
$string['feedback:addinstance'] = 'Add a new feedback';
$string['feedbackclose'] = 'Allow answers to';
$string['feedbackclose'] = 'Allow answers until';
$string['feedback:complete'] = 'Complete a feedback';
$string['feedback:createprivatetemplate'] = 'Create private template';
$string['feedback:createpublictemplate'] = 'Create public template';
@ -256,6 +256,7 @@ $string['required'] = 'Required';
$string['resetting_data'] = 'Responses';
$string['resetting_delete'] = 'Delete responses';
$string['resetting_feedbacks'] = 'Resetting feedbacks';
$string['responded'] = 'Responded';
$string['response_nr'] = 'Response number';
$string['responses'] = 'Responses';
$string['responsetime'] = 'Responses time';
@ -295,7 +296,7 @@ $string['textfield'] = 'Short text answer';
$string['textfield_maxlength'] = 'Maximum characters accepted';
$string['textfield_size'] = 'Textfield width';
$string['there_are_no_settings_for_recaptcha'] = 'There are no settings for captcha';
$string['this_feedback_is_already_submitted'] = 'You\'ve already completed this activity.';
$string['this_feedback_is_already_submitted'] = 'You have already submitted this feedback.';
$string['typemissing'] = 'Missing value "type"';
$string['update_item'] = 'Save changes to question';
$string['url_for_continue'] = 'Link to next activity';

View File

@ -0,0 +1,89 @@
@mod @mod_feedback
Feature: Testing overview integration in mod_feedback
In order to list all feedbacks in a course
As a user
I need to be able to see the feedback overview
Background:
Given the following "users" exist:
| username | firstname | lastname |
| student1 | Username | 1 |
| student2 | Username | 2 |
| student3 | Username | 3 |
| student4 | Username | 4 |
| student5 | Username | 5 |
| student6 | Username | 6 |
| student7 | Username | 7 |
| student8 | Username | 8 |
| teacher1 | Teacher | T |
And the following "courses" exist:
| fullname | shortname | groupmode |
| Course 1 | C1 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
| student4 | C1 | student |
| student5 | C1 | student |
| student6 | C1 | student |
| student7 | C1 | student |
| student8 | C1 | student |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | course | idnumber | timeclose |
| feedback | Date feedback | C1 | feedback1 | ##1 Jan 2040 08:00## |
| feedback | Not responded feedback | C1 | feedback2 | ##1 Jan 2040 08:00## |
| feedback | No date feedback | C1 | feedback3 | |
Given the following "mod_feedback > question" exists:
| activity | feedback1 |
| name | Do you like this course? |
| questiontype | multichoice |
| label | multichoice1 |
| subtype | r |
| hidenoselect | 1 |
| values | Yes of course\nNot at all\nI don't know |
And the following "mod_feedback > responses" exist:
| activity | user | Do you like this course? |
| feedback1 | student1 | Not at all |
| feedback1 | student2 | I don't know |
| feedback1 | student3 | Not at all |
| feedback1 | student4 | Yes of course |
| feedback3 | student1 | Not at all |
| feedback3 | student2 | I don't know |
| feedback3 | student3 | Not at all |
Scenario: Teacher can see the feedback relevant information in the feedback overview
When I am on the "Course 1" "course > activities > feedback" page logged in as "teacher1"
Then I should see "Responses" in the "feedback_overview_collapsible" "region"
And I should not see "Responded" in the "feedback_overview_collapsible" "region"
And I should see "Allow answers until" in the "feedback_overview_collapsible" "region"
And I should see "1 January 2040" in the "Date feedback" "table_row"
And I should see "4 of 8" in the "Date feedback" "table_row"
And I should see "1 January 2040" in the "Not responded feedback" "table_row"
And I should see "0 of 8" in the "Not responded feedback" "table_row"
And I should see "-" in the "No date feedback" "table_row"
And I should see "3 of 8" in the "No date feedback" "table_row"
And I click on "4 of 8" "link" in the "Date feedback" "table_row"
And I should see "Show responses"
Scenario: Students can see the feedback relevant information in the feedback overview
When I am on the "Course 1" "course > activities > feedback" page logged in as "student1"
Then I should not see "Responses" in the "feedback_overview_collapsible" "region"
And I should see "Responded" in the "feedback_overview_collapsible" "region"
And I should see "Allow answers until" in the "feedback_overview_collapsible" "region"
And I should see "1 January 2040" in the "Date feedback" "table_row"
And "You have already submitted this feedback" "icon" should exist in the "Date feedback" "table_row"
And I should see "1 January 2040" in the "Not responded feedback" "table_row"
And I should see "-" in the "Not responded feedback" "table_row"
And I should see "-" in the "No date feedback" "table_row"
And "You have already submitted this feedback" "icon" should exist in the "No date feedback" "table_row"
Scenario: The feedback overview report should generate log events
Given I am on the "Course 1" "course > activities > feedback" page logged in as "teacher1"
When I am on the "Course 1" "course" page logged in as "teacher1"
And I navigate to "Reports" in current page administration
And I click on "Logs" "link"
And I click on "Get these logs" "button"
Then I should see "Course activities overview page viewed"
And I should see "viewed the instance list for the module 'feedback'"

View File

@ -0,0 +1,269 @@
<?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 mod_feedback\courseformat;
use core_courseformat\local\overview\overviewfactory;
/**
* Tests for Feedback
*
* @covers \mod_feedback\courseformat\overview
* @package mod_feedback
* @category test
* @copyright 2025 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class overview_test extends \advanced_testcase {
#[\Override]
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/mod/feedback/lib.php');
parent::setUpBeforeClass();
}
/**
* Test get_actions_overview.
*
* @covers ::get_actions_overview
* @dataProvider provider_test_get_actions_overview
*
* @param string $user
* @param bool $expectnull
* @param bool $hasresponses
* @return void
*/
public function test_get_actions_overview(string $user, bool $expectnull, bool $hasresponses): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
$activity = $this->getDataGenerator()->create_module(
'feedback',
['course' => $course->id],
);
$cm = get_fast_modinfo($course)->get_cm($activity->cmid);
$feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
$itemcreated = $feedbackgenerator->create_item_multichoice($activity, ['values' => "y\nn"]);
$expectedresonses = 0;
if ($hasresponses) {
$this->setUser($student);
$feedbackgenerator->create_response([
'userid' => $student->id,
'cmid' => $cm->id,
'anonymous' => false,
$itemcreated->name => 'y',
]);
$expectedresonses = 1;
}
$currentuser = ($user == 'teacher') ? $teacher : $student;
$this->setUser($currentuser);
$item = overviewfactory::create($cm)->get_actions_overview();
// Students should not see item.
if ($expectnull) {
$this->assertNull($item);
return;
}
// Teachers should see item.
$this->assertEquals(get_string('responses', 'mod_feedback'), $item->get_name());
$this->assertEquals($expectedresonses, $item->get_value());
}
/**
* Data provider for test_get_actions_overview.
*
* @return array
*/
public static function provider_test_get_actions_overview(): array {
return [
'Teacher with responses' => [
'user' => 'teacher',
'expectnull' => false,
'hasresponses' => true,
],
'Student with responses' => [
'user' => 'student',
'expectnull' => true,
'hasresponses' => true,
],
'Teacher without responses' => [
'user' => 'teacher',
'expectnull' => false,
'hasresponses' => false,
],
'Student without responses' => [
'user' => 'student',
'expectnull' => true,
'hasresponses' => false,
],
];
}
/**
* Test get_due_date_overview.
* @covers ::get_due_date_overview
* @dataProvider provider_test_get_due_date_overview
* @param string $user
* @param bool $hasduedate
* @return void
*/
public function test_get_due_date_overview(string $user, bool $hasduedate): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
$moddata = [
'course' => $course->id,
'timeclose' => $hasduedate ? time() + 3600 : 0,
];
$activity = $this->getDataGenerator()->create_module('feedback', $moddata);
$cm = get_fast_modinfo($course)->get_cm($activity->cmid);
$currentuser = ($user == 'teacher') ? $teacher : $student;
$this->setUser($currentuser);
$item = overviewfactory::create($cm)->get_due_date_overview();
// Teachers should see item.
$this->assertEquals(get_string('feedbackclose', 'mod_feedback'), $item->get_name());
$expectedvalue = $hasduedate ? $moddata['timeclose'] : null;
$this->assertEquals($expectedvalue, $item->get_value());
}
/**
* Data provider for test_get_due_date_overview.
*
* @return array
*/
public static function provider_test_get_due_date_overview(): array {
return [
'Teacher with due date' => [
'user' => 'teacher',
'hasduedate' => true,
],
'Student with due date' => [
'user' => 'student',
'hasduedate' => true,
],
'Teacher without due date' => [
'user' => 'teacher',
'hasduedate' => false,
],
'Student without due date' => [
'user' => 'student',
'hasduedate' => false,
],
];
}
/**
* Test get_extra_submitted_overview.
*
* @covers ::get_extra_submitted_overview
* @dataProvider provider_test_get_extra_submitted_overview
*
* @param string $user
* @param bool $expectnull
* @param bool $hasresponses
* @return void
*/
public function test_get_extra_submitted_overview(string $user, bool $expectnull, bool $hasresponses): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
$activity = $this->getDataGenerator()->create_module(
'feedback',
['course' => $course->id],
);
$cm = get_fast_modinfo($course)->get_cm($activity->cmid);
$feedbackgenerator = $this->getDataGenerator()->get_plugin_generator('mod_feedback');
$itemcreated = $feedbackgenerator->create_item_multichoice($activity, ['values' => "y\nn"]);
$expectedresonses = 0;
if ($hasresponses) {
$this->setUser($student);
$feedbackgenerator->create_response([
'userid' => $student->id,
'cmid' => $cm->id,
'anonymous' => false,
$itemcreated->name => 'y',
]);
$expectedresonses = 1;
}
$currentuser = ($user == 'teacher') ? $teacher : $student;
$this->setUser($currentuser);
$overview = overviewfactory::create($cm);
$reflection = new \ReflectionClass($overview);
$method = $reflection->getMethod('get_extra_submitted_overview');
$method->setAccessible(true);
$item = $method->invoke($overview);
// Students should not see item.
if ($expectnull) {
$this->assertNull($item);
return;
}
// Teachers should see item.
$this->assertEquals(get_string('responded', 'mod_feedback'), $item->get_name());
$this->assertEquals($hasresponses, $item->get_value());
}
/**
* Data provider for test_get_extra_submitted_overview.
*
* @return array
*/
public static function provider_test_get_extra_submitted_overview(): array {
return [
'Teacher with responses' => [
'user' => 'teacher',
'expectnull' => true,
'hasresponses' => true,
],
'Student with responses' => [
'user' => 'student',
'expectnull' => false,
'hasresponses' => true,
],
'Teacher without responses' => [
'user' => 'teacher',
'expectnull' => true,
'hasresponses' => false,
],
'Student without responses' => [
'user' => 'student',
'expectnull' => false,
'hasresponses' => false,
],
];
}
}

View File

@ -141,7 +141,11 @@ if ($feedbackcompletion->can_complete()) {
echo $OUTPUT->continue_button(course_get_url($courseid ?: $course->id));
} else if (!$feedbackcompletion->can_submit()) {
// Feedback was already submitted.
echo $OUTPUT->notification(get_string('this_feedback_is_already_submitted', 'feedback'));
echo $OUTPUT->notification(
get_string('this_feedback_is_already_submitted', 'feedback'),
\core\output\notification::NOTIFY_INFO,
closebutton: false,
);
$OUTPUT->continue_button(course_get_url($courseid ?: $course->id));
}
echo $OUTPUT->box_end();