From 673a247b0dcc60d35737eafc07a3cc2e398df1f6 Mon Sep 17 00:00:00 2001 From: Amaia Anabitarte Date: Mon, 2 Jun 2025 17:31:07 +0200 Subject: [PATCH] MDL-83895 mod_h5pactivity: New overview page --- .../local/overview/overviewfactory_test.php | 2 +- .../overview/missingoverviewnotice_test.php | 2 +- .../classes/courseformat/overview.php | 204 +++++++++++++++ mod/h5pactivity/lang/en/h5pactivity.php | 6 + .../tests/behat/overview_report.feature | 94 +++++++ .../tests/courseformat/overview_test.php | 247 ++++++++++++++++++ 6 files changed, 553 insertions(+), 2 deletions(-) create mode 100644 mod/h5pactivity/classes/courseformat/overview.php create mode 100644 mod/h5pactivity/tests/behat/overview_report.feature create mode 100644 mod/h5pactivity/tests/courseformat/overview_test.php diff --git a/course/format/tests/local/overview/overviewfactory_test.php b/course/format/tests/local/overview/overviewfactory_test.php index 5982887f03c..7efa1d263f6 100644 --- a/course/format/tests/local/overview/overviewfactory_test.php +++ b/course/format/tests/local/overview/overviewfactory_test.php @@ -117,7 +117,7 @@ final class overviewfactory_test extends \advanced_testcase { ], 'h5pactivity' => [ 'resourcetype' => 'h5pactivity', - 'expected' => resourceoverview::class, + 'expected' => \mod_h5pactivity\courseformat\overview::class, ], 'lesson' => [ 'resourcetype' => 'lesson', diff --git a/course/format/tests/output/local/overview/missingoverviewnotice_test.php b/course/format/tests/output/local/overview/missingoverviewnotice_test.php index 2b1022550ee..3b4dbe5e42f 100644 --- a/course/format/tests/output/local/overview/missingoverviewnotice_test.php +++ b/course/format/tests/output/local/overview/missingoverviewnotice_test.php @@ -72,7 +72,7 @@ final class missingoverviewnotice_test extends \advanced_testcase { 'folder' => ['modname' => 'folder', 'expectempty' => false], 'forum' => ['modname' => 'forum', 'expectempty' => false], 'glossary' => ['modname' => 'glossary', 'expectempty' => true], - 'h5pactivity' => ['modname' => 'h5pactivity', 'expectempty' => false], + 'h5pactivity' => ['modname' => 'h5pactivity', 'expectempty' => true], 'imscp' => ['modname' => 'imscp', 'expectempty' => false], 'label' => ['modname' => 'label', 'expectempty' => false], 'lesson' => ['modname' => 'lesson', 'expectempty' => false], diff --git a/mod/h5pactivity/classes/courseformat/overview.php b/mod/h5pactivity/classes/courseformat/overview.php new file mode 100644 index 00000000000..5c152416c56 --- /dev/null +++ b/mod/h5pactivity/classes/courseformat/overview.php @@ -0,0 +1,204 @@ +. + +namespace mod_h5pactivity\courseformat; + +use cm_info; +use core_courseformat\local\overview\overviewitem; +use core\output\action_link; +use core\output\local\properties\text_align; +use core\output\local\properties\button; +use core\url; +use core_courseformat\output\local\overview\overviewdialog; +use mod_h5pactivity\local\manager; + +/** + * H5P activity overview integration. + * + * @package mod_h5pactivity + * @copyright 2025 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class overview extends \core_courseformat\activityoverviewbase { + + /** @var manager H5P activity manager. */ + private $manager; + + /** + * Constructor. + * + * @param cm_info $cm the course module instance. + * @param \core\output\renderer_helper $rendererhelper the renderer helper. + * @param \core_string_manager $stringmanager the string manager. + */ + public function __construct( + cm_info $cm, + /** @var \core\output\renderer_helper $rendererhelper the renderer helper */ + protected readonly \core\output\renderer_helper $rendererhelper, + /** @var \core_string_manager $stringmanager the string manager */ + protected readonly \core_string_manager $stringmanager, + ) { + parent::__construct($cm); + + $this->manager = manager::create_from_coursemodule($cm); + } + + #[\Override] + public function get_actions_overview(): ?overviewitem { + + if (!$this->manager->can_view_all_attempts()) { + return null; + } + + $viewresults = $this->stringmanager->get_string('viewresults', 'mod_h5pactivity'); + $content = new action_link( + url: new url('/mod/h5pactivity/report.php', ['id' => $this->cm->id]), + text: $viewresults, + attributes: ['class' => button::SECONDARY_OUTLINE->classes()], + ); + + return new overviewitem( + name: $this->stringmanager->get_string('actions'), + value: '', + content: $content, + textalign: text_align::CENTER, + ); + } + + #[\Override] + public function get_extra_overview_items(): array { + global $USER; + + if (!$this->manager->can_view_own_attempts() && $this->manager->can_view_all_attempts()) { + return []; + } + + if (!$this->manager->can_view_all_attempts()) { + return [ + 'myattempts' => $this->get_extra_userattempts_overview($USER->id), + ]; + } + + return [ + 'h5ptype' => $this->get_extra_h5ptype_overview(), + 'attempted' => $this->get_extra_studentsattempted_overview(), + 'totalattempts' => $this->get_extra_totalattempts_overview(), + ]; + } + + /** + * Get the attempts of the given user. + * + * @param int $userid The user to return the attempts from. + * @return overviewitem The overview item. + */ + private function get_extra_userattempts_overview(int $userid): overviewitem { + + $attempts = $this->manager->count_attempts($userid); + return new overviewitem( + name: $this->stringmanager->get_string('attempts', 'mod_h5pactivity'), + value: $attempts, + content: $attempts ?? '-', + textalign: text_align::CENTER, + ); + } + + /** + * Get the students who attempted. + * + * @return overviewitem The overview item. + */ + private function get_extra_studentsattempted_overview(): overviewitem { + + $attempts = $this->manager->count_users_attempts(); + $participants = get_users_by_capability($this->context, 'mod/h5pactivity:submit'); + $params = [ + 'count' => count($attempts), + 'total' => count($participants), + ]; + return new overviewitem( + name: $this->stringmanager->get_string('attempted', 'mod_h5pactivity'), + value: count($attempts), + content: $this->stringmanager->get_string('count_of_total', 'core', $params), + textalign: text_align::CENTER, + ); + } + + /** + * Get the "Total attempts" colum data. + * + * @return overviewitem The overview item. + */ + private function get_extra_totalattempts_overview(): overviewitem { + $totalattempts = $this->manager->count_attempts(); + $totalusers = $this->manager->count_users_attempts(); + + $content = '-'; + if ($totalusers && count($totalusers) > 0 && $totalattempts) { + $averageattempts = (int) round($totalattempts / count($totalusers)); + $content = new overviewdialog( + buttoncontent: $totalattempts, + title: $this->stringmanager->get_string('totalattempts', 'mod_h5pactivity'), + definition: ['buttonclasses' => button::SECONDARY_OUTLINE->classes() . ' dropdown-toggle'], + ); + $method = $this->manager::get_grading_methods()[$this->manager->get_instance()->grademethod]; + $content->add_item($this->stringmanager->get_string('gradingmethod', 'grading'), $method); + $content->add_item($this->stringmanager->get_string('averageattempts', 'mod_h5pactivity'), $averageattempts); + } + + return new overviewitem( + name: $this->stringmanager->get_string('totalattempts', 'mod_h5pactivity'), + value: $totalattempts, + content: $content, + ); + } + + /** + * Get the H5P content type. + * + * @return overviewitem The overview item. + */ + private function get_extra_h5ptype_overview(): overviewitem { + $fs = get_file_storage(); + $files = $fs->get_area_files($this->context->id, 'mod_h5pactivity', 'package', 0, 'id', false); + $file = reset($files); + + $h5p = \core_h5p\api::get_content_from_pathnamehash($file->get_pathnamehash()); + + $unknonwoverview = new overviewitem( + name: $this->stringmanager->get_string('contenttype', 'mod_h5pactivity'), + value: $this->stringmanager->get_string('unknowntype', 'mod_h5pactivity'), + content: $this->stringmanager->get_string('unknowntype', 'mod_h5pactivity'), + ); + + if (empty($h5p)) { + return $unknonwoverview; + } + + $h5plib = \core_h5p\api::get_library($h5p->mainlibraryid); + + // If the content is not yet deployed we cannot show the content type. + if (empty($h5plib)) { + return $unknonwoverview; + } + + return new overviewitem( + name: $this->stringmanager->get_string('contenttype', 'mod_h5pactivity'), + value: $h5plib->title, + content: $h5plib->title, + ); + } +} diff --git a/mod/h5pactivity/lang/en/h5pactivity.php b/mod/h5pactivity/lang/en/h5pactivity.php index e81722784c5..fc61b467173 100644 --- a/mod/h5pactivity/lang/en/h5pactivity.php +++ b/mod/h5pactivity/lang/en/h5pactivity.php @@ -37,6 +37,7 @@ $string['answer_text'] = 'Answer text'; $string['answer_noanswer'] = 'None'; $string['areapackage'] = 'Package file'; $string['attempt'] = 'Attempt'; +$string['attempted'] = 'Students who attempted'; $string['attempt_average'] = 'Attempts average scored'; $string['attempt_answer'] = 'Attempt answer'; $string['attempt_completion_no'] = 'This attempt is not marked as completed'; @@ -53,10 +54,12 @@ $string['attempts'] = 'Attempts'; $string['attempts_report_header_label'] = 'Attempts ({$a})'; $string['attempts_report'] = 'Attempts report'; $string['attempts_none'] = 'This user has no attempts to display.'; +$string['averageattempts'] = 'Average attempts per student'; $string['choice'] = 'Choice'; $string['completion'] = 'Completion'; $string['contentbank'] = 'More information about the content bank'; $string['contentbank_help'] = 'In the content bank you can create and store content using several authoring tools, including an integrated H5P creator.'; +$string['contenttype'] = 'H5P type'; $string['correct_answer'] = 'Correct answer'; $string['deleteallattempts'] = 'All H5P attempts'; $string['displayexport'] = 'Allow download'; @@ -140,12 +143,15 @@ $string['score_out_of'] = '{$a->rawscore} out of {$a->maxscore}'; $string['search:activity'] = 'H5P - activity information'; $string['startdate'] = 'Start date'; $string['statement_received'] = 'xAPI statement received'; +$string['totalattempts'] = 'Total attempts'; $string['totalscore'] = 'Total score'; $string['trackingdisabled'] = 'Attempt tracking is not enabled for this activity.'; $string['trackingdisabled_enable'] = 'Attempt tracking is not enabled for this activity. You can enable it in Settings.'; $string['tracking_messages'] = 'Some H5P provide attempt tracking data for advanced reporting such as number of attempts, responses and grades. Note: Some H5P don\'t provide attempt tracking data. In such cases, the following settings will have no effect.'; $string['true'] = 'True'; +$string['unknowntype'] = 'Unknown H5P type'; $string['usecontentbank'] = 'Use the content bank (opens in new window) to manage your H5P files'; $string['view'] = 'View'; $string['viewattempts'] = 'View attempts ({$a})'; +$string['viewresults'] = 'View results'; $string['view_report'] = 'View report'; diff --git a/mod/h5pactivity/tests/behat/overview_report.feature b/mod/h5pactivity/tests/behat/overview_report.feature new file mode 100644 index 00000000000..535e006aaf5 --- /dev/null +++ b/mod/h5pactivity/tests/behat/overview_report.feature @@ -0,0 +1,94 @@ +@mod @mod_h5pactivity +Feature: Testing overview integration in H5P activity + In order to summarize the H5P activity + As a user + I need to be able to see the H5P activity overview + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + | student3 | Student | 3 | student3@example.com | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | enablecompletion | + | Course 1 | C1 | 0 | 1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + And the following "activity" exists: + | course | C1 | + | activity | h5pactivity | + | name | H5P activity | + | intro | description | + | packagefilepath | h5p/tests/fixtures/find-the-words.h5p | + | idnumber | h5p | + | completion | 1 | + | enabletracking | 1 | + | reviewmode | 1 | + | grademethod | 2 | + And the following "activity" exists: + | course | C1 | + | activity | h5pactivity | + | name | Empty H5P activity | + | intro | empty | + | idnumber | empty | + And the following "mod_h5pactivity > attempts" exist: + | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | + # student1. + | student1 | H5P activity | 1 | choice | 2 | 2 | 1 | 1 | 1 | + | student1 | H5P activity | 1 | compound | 2 | 2 | 4 | 1 | 1 | + | student1 | H5P activity | 2 | choice | 0 | 2 | 1 | 1 | 0 | + | student1 | H5P activity | 2 | compound | 0 | 2 | 4 | 1 | 0 | + | student1 | H5P activity | 3 | matching | 2 | 2 | 1 | 1 | 1 | + | student1 | H5P activity | 3 | compound | 2 | 2 | 4 | 1 | 1 | + | student1 | H5P activity | 4 | true-false | 2 | 2 | 1 | 1 | 1 | + | student1 | H5P activity | 4 | compound | 2 | 2 | 4 | 1 | 1 | + # student2. + | student2 | H5P activity | 1 | compound | 0 | 2 | 1 | 1 | 0 | + # We need to navigate to the activity to deploy the H5P file. + And I am on the "H5P activity" "h5pactivity activity" page logged in as admin + And I log out + + Scenario: The H5P activity overview report should generate log events + Given I am on the "Course 1" "course > activities > h5pactivity" page logged in as "teacher1" + And 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" + When 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 'h5pactivity'" + + @javascript + Scenario: Students can see relevant columns in the H5P activity overview + Given I am on the "Course 1" "course > activities > h5pactivity" page logged in as student1 + # Check columns. + When I should see "Completion status" in the "h5pactivity_overview_collapsible" "region" + # Check column values. + Then the following should exist in the "Table listing all H5P activities" table: + | Name | Attempts | Grade | + | H5P activity | 4 | 75.00 | + | Empty H5P activity | 0 | - | + + @javascript + Scenario: Teachers can see relevant columns in the H5P activity overview + Given I am on the "Course 1" "course > activities > h5pactivity" page logged in as teacher1 + # Check columns. + And I should see "Total attempts" in the "h5pactivity_overview_collapsible" "region" + # Check column values. + And the following should exist in the "Table listing all H5P activities" table: + | Name | H5P type | Students who attempted | Total attempts | Actions | + | H5P activity | Find The Words | 2 of 3 | 5 | View results | + | Empty H5P activity | Unknown H5P type | 0 of 3 | - | View results | + # Check the Total attempts value. + And I should see "-" in the "Empty H5P activity" "table_row" + When I click on "5" "button" in the "H5P activity" "table_row" + Then I should see "Grading method: Average grade" + And I should see "Average attempts per student: 3" + # Check the View results link. + And I click on "View results" "link" in the "H5P activity" "table_row" + And I should see "Attempts (5)" diff --git a/mod/h5pactivity/tests/courseformat/overview_test.php b/mod/h5pactivity/tests/courseformat/overview_test.php new file mode 100644 index 00000000000..c86665dadd7 --- /dev/null +++ b/mod/h5pactivity/tests/courseformat/overview_test.php @@ -0,0 +1,247 @@ +. + +namespace mod_h5pactivity\courseformat; + +use core_courseformat\local\overview\overviewfactory; + +/** + * Tests for H5P activity overview + * + * @covers \mod_h5pactivity\courseformat\overview + * @package mod_h5pactivity + * @category test + * @copyright 2025 Amaia Anabitarte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class overview_test extends \advanced_testcase { + + /** + * Test get_actions_overview. + * + * @covers ::get_actions_overview + */ + public function test_get_actions_overview(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module( + 'h5pactivity', + ['course' => $course, 'enabletracking' => 1], + ); + $cm = get_fast_modinfo($course)->get_cm($activity->cmid); + + // Prepare users: 1 teacher, 2 students, 1 unenroled user. + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + + $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); + + // Students have no action column. + $this->setUser($student); + $this->assertNull(overviewfactory::create($cm)->get_actions_overview()); + + // Teachers have a 'View results' button. + $this->setUser($teacher); + $items = overviewfactory::create($cm)->get_actions_overview(); + $this->assertNotNull($items); + $this->assertEquals(get_string('actions'), $items->get_name()); + } + + /** + * Test get_extra_h5ptype_overview. + * + * @covers ::get_extra_h5ptype_overview + * @dataProvider provider_test_get_extra_h5type_overview + * + * @param string $h5pfile + * @param bool $iscorrect + * @param string $expected + * @return void + */ + public function test_get_extra_h5ptype_overview( + string $h5pfile, + bool $iscorrect, + string $expected + ): void { + global $CFG; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $params = [ + 'course' => $course->id, + 'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/'.$h5pfile, + 'introformat' => 1, + ]; + $activity = $this->getDataGenerator()->create_module('h5pactivity', $params); + // Add filename and contextid to make easier the asserts. + $activity->filename = $h5pfile; + $context = \context_module::instance($activity->cmid); + $activity->contextid = $context->id; + + // Create a fake deploy H5P file. + + /** @var \core_h5p_generator $h5pgenerator */ + $h5pgenerator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); + + if (!$iscorrect) { + $this->expectException(\TypeError::class); + } + $h5pgenerator->create_export_file($activity->filename, $context->id, 'mod_h5pactivity', 'package'); + + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + $this->setUser($teacher); + + $cm = get_fast_modinfo($course)->get_cm($activity->cmid); + $items = overviewfactory::create($cm)->get_extra_overview_items(); + + $this->assertEquals($expected, $items['h5ptype']->get_value()); + + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $this->setUser($student); + + $cm = get_fast_modinfo($course)->get_cm($activity->cmid); + $items = overviewfactory::create($cm)->get_extra_overview_items(); + + $this->assertArrayNotHasKey('h5ptype', $items); + } + + /** + * Data provider for test h5p type overview extra. + * + * @return array + */ + public static function provider_test_get_extra_h5type_overview(): array { + return [ + 'Basic package' => [ + 'h5pfile' => 'basic_essay.h5p', + 'iscorrect' => true, + 'expected' => 'Essay', + ], + 'No json file' => [ + 'h5pfile' => 'no-json-file.h5p', + 'iscorrect' => false, + 'expected' => get_string('unknowntype', 'mod_h5pactivity'), + ], + 'Unzippable package' => [ + 'h5pfile' => 'unzippable.h5p', + 'iscorrect' => false, + 'expected' => get_string('unknowntype', 'mod_h5pactivity'), + ], + ]; + } + + /** + * Test get_extra_overview_items. + * + * @covers ::get_extra_overview_items + */ + public function test_get_extra_attempts_overview(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module( + 'h5pactivity', + ['course' => $course, 'enabletracking' => 1], + ); + $cm = get_fast_modinfo($course)->get_cm($activity->cmid); + + // Prepare users: 1 teacher, 2 students, 1 unenroled user. + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $other = $this->getDataGenerator()->create_and_enrol($course, 'student'); + + $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); + + // No attempts yet. + $this->setUser($teacher); + $items = overviewfactory::create($cm)->get_extra_overview_items(); + $this->assertEquals(0, $items['totalattempts']->get_value()); + + $this->setUser($student); + $items = overviewfactory::create($cm)->get_extra_overview_items(); + $this->assertEquals(0, $items['myattempts']->get_value()); + + // Attempts done by other student. + $params = ['cmid' => $cm->id, 'userid' => $other->id]; + $generator->create_content($activity, $params); + $generator->create_content($activity, $params); + + $this->setUser($teacher); + $items = overviewfactory::create($cm)->get_extra_overview_items(); + $this->assertEquals(2, $items['totalattempts']->get_value()); + + $this->setUser($student); + $items = overviewfactory::create($cm)->get_extra_overview_items(); + $this->assertEquals(0, $items['myattempts']->get_value()); + + // Attempts done by the student. + $params = ['cmid' => $cm->id, 'userid' => $student->id]; + $generator->create_content($activity, $params); + $generator->create_content($activity, $params); + + $this->setUser($teacher); + $items = overviewfactory::create($cm)->get_extra_overview_items(); + $this->assertEquals(4, $items['totalattempts']->get_value()); + + $this->setUser($student); + $items = overviewfactory::create($cm)->get_extra_overview_items(); + $this->assertEquals(2, $items['myattempts']->get_value()); + } + + /** + * Test get_extra_studentsattempted_overview. + * + * @covers ::get_extra_studentsattempted_overview + */ + public function test_get_extra_studentsattempted_overview(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module( + 'h5pactivity', + ['course' => $course, 'enabletracking' => 1], + ); + $cm = get_fast_modinfo($course)->get_cm($activity->cmid); + + // Prepare users: 1 teacher, 2 students, 1 unenroled user. + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $other = $this->getDataGenerator()->create_and_enrol($course, 'student'); + + $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); + + // No attempts yet. + $this->setUser($teacher); + $items = overviewfactory::create($cm)->get_extra_overview_items(); + $this->assertEquals(0, $items['attempted']->get_value()); + $this->assertEquals('0 of 2', $items['attempted']->get_content()); + + // With attempts. + $params = ['cmid' => $cm->id, 'userid' => $student->id]; + $generator->create_content($activity, $params); + $generator->create_content($activity, $params); + + $items = overviewfactory::create($cm)->get_extra_overview_items(); + $this->assertEquals(1, $items['attempted']->get_value()); + $this->assertEquals('1 of 2', $items['attempted']->get_content()); + } +}