From 7d594cb0d3e6aa92bbc01fa2bbb70b891399509b Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Thu, 25 Mar 2021 15:39:05 +0800 Subject: [PATCH] MDL-70935 mod_lesson: Custom completion implementation --- .../classes/completion/custom_completion.php | 94 +++++++ mod/lesson/lang/en/lesson.php | 2 + mod/lesson/tests/custom_completion_test.php | 260 ++++++++++++++++++ 3 files changed, 356 insertions(+) create mode 100644 mod/lesson/classes/completion/custom_completion.php create mode 100644 mod/lesson/tests/custom_completion_test.php diff --git a/mod/lesson/classes/completion/custom_completion.php b/mod/lesson/classes/completion/custom_completion.php new file mode 100644 index 00000000000..133581261fc --- /dev/null +++ b/mod/lesson/classes/completion/custom_completion.php @@ -0,0 +1,94 @@ +. + +declare(strict_types=1); + +namespace mod_lesson\completion; + +use core_completion\activity_custom_completion; + +/** + * Activity custom completion subclass for the lesson activity. + * + * Contains the class for defining mod_lesson's custom completion rules + * and fetching a lesson instance's completion statuses for a user. + * + * @package mod_lesson + * @copyright Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class custom_completion extends activity_custom_completion { + + /** + * Fetches the completion state for a given completion rule. + * + * @param string $rule The completion rule. + * @return int The completion state. + */ + public function get_state(string $rule): int { + global $DB; + + $this->validate_rule($rule); + + switch ($rule) { + case 'completiontimespent': + $duration = $DB->get_field_sql( + "SELECT SUM(lessontime - starttime) + FROM {lesson_timer} + WHERE lessonid = :lessonid + AND userid = :userid", + ['userid' => $this->userid, 'lessonid' => $this->cm->instance]); + + $status = ($duration && $duration >= $this->cm->customdata['customcompletionrules']['completiontimespent']); + break; + case 'completionendreached': + $status = $DB->record_exists('lesson_timer', + ['lessonid' => $this->cm->instance, 'userid' => $this->userid, 'completed' => 1]); + break; + default: + $status = false; + break; + } + + return $status ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE; + } + + /** + * Fetch the list of custom completion rules that this module defines. + * + * @return array + */ + public static function get_defined_custom_rules(): array { + return [ + 'completiontimespent', + 'completionendreached', + ]; + } + + /** + * Returns an associative array of the descriptions of custom completion rules. + * + * @return array + */ + public function get_custom_rule_descriptions(): array { + $timespent = format_time($this->cm->customdata['customcompletionrules']['completiontimespent'] ?? 0); + + return [ + 'completiontimespent' => get_string('completiondetail:timespent', 'lesson', $timespent), + 'completionendreached' => get_string('completiondetail:reachend', 'lesson'), + ]; + } +} diff --git a/mod/lesson/lang/en/lesson.php b/mod/lesson/lang/en/lesson.php index 36dd037dd7d..c4f4b2499a8 100644 --- a/mod/lesson/lang/en/lesson.php +++ b/mod/lesson/lang/en/lesson.php @@ -113,6 +113,8 @@ $string['comments'] = 'Your comments'; $string['completed'] = 'Completed'; $string['completederror'] = 'Complete the lesson'; $string['completethefollowingconditions'] = 'You must complete the following condition(s) in {$a} lesson before you can proceed.'; +$string['completiondetail:reachend'] = 'Go through the activity to the end'; +$string['completiondetail:timespent'] = 'Spend at least {$a} on this activity'; $string['completionendreached'] = 'Require end reached'; $string['completionendreached_desc'] = 'Student must reach the end of lesson page to complete this activity'; $string['completiontimespent'] = 'Student must do this activity at least for'; diff --git a/mod/lesson/tests/custom_completion_test.php b/mod/lesson/tests/custom_completion_test.php new file mode 100644 index 00000000000..4ad90941e42 --- /dev/null +++ b/mod/lesson/tests/custom_completion_test.php @@ -0,0 +1,260 @@ +. + +declare(strict_types = 1); + +namespace mod_lesson; + +use advanced_testcase; +use cm_info; +use coding_exception; +use mod_lesson\completion\custom_completion; +use moodle_exception; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->libdir . '/completionlib.php'); + +/** + * Class for unit testing mod_lesson/custom_completion. + * + * @package mod_lesson + * @copyright 2021 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class custom_completion_test extends advanced_testcase { + + /** + * Data provider for get_state(). + * + * @return array[] + */ + public function get_state_provider(): array { + return [ + 'Undefined completion requirement' => [ + 'somenonexistentrule', COMPLETION_ENABLED, 3, null, coding_exception::class + ], + 'Minimum time spent requirement not available' => [ + 'completionstatusrequired', COMPLETION_DISABLED, 3, null, moodle_exception::class + ], + 'Minimum time spent required, user has not spent time in the lesson' => [ + 'completiontimespent', 30, false, COMPLETION_INCOMPLETE, null + ], + 'Minimum time spent required, user has not met completion requirement' => [ + 'completiontimespent', 30, 10, COMPLETION_INCOMPLETE, null + ], + 'Minimum time spent required, user has met completion requirement' => [ + 'completiontimespent', 30, 30, COMPLETION_COMPLETE, null + ], + 'Minimum time spent required, user has exceeded completion requirement' => [ + 'completiontimespent', 30, 40, COMPLETION_COMPLETE, null + ], + 'User must reach end of lesson, has not met completion requirement' => [ + 'completionendreached', 1, false, COMPLETION_INCOMPLETE, null + ], + 'User must reach end of lesson, has not met completion requirement' => [ + 'completionendreached', 1, true, COMPLETION_COMPLETE, null + ], + ]; + } + + /** + * Test for get_state(). + * + * @dataProvider get_state_provider + * @param string $rule The custom completion condition. + * @param int $rulevalue The custom completion rule value. + * @param mixed $uservalue The database value returned when checking the rule for the user. + * @param int|null $status Expected completion status for the rule. + * @param string|null $exception Expected exception. + */ + public function test_get_state(string $rule, int $rulevalue, $uservalue, ?int $status, ?string $exception) { + global $DB; + + if (!is_null($exception)) { + $this->expectException($exception); + } + + // Custom completion rule data for cm_info::customdata. + $customdataval = [ + 'customcompletionrules' => [ + $rule => $rulevalue + ] + ]; + + // Build a mock cm_info instance. + $mockcminfo = $this->getMockBuilder(cm_info::class) + ->disableOriginalConstructor() + ->onlyMethods(['__get']) + ->getMock(); + + // Mock the return of the magic getter method when fetching the cm_info object's + // customdata and instance values. + $mockcminfo->expects($this->any()) + ->method('__get') + ->will($this->returnValueMap([ + ['customdata', $customdataval], + ['instance', 1], + ])); + + if ($rule === 'completiontimespent') { + // Mock the DB call fetching user's lesson time spent. + $DB = $this->createMock(get_class($DB)); + $DB->expects($this->atMost(1)) + ->method('get_field_sql') + ->willReturn($uservalue); + } else if ($rule === 'completionendreached') { + // Mock the DB call fetching user's end reached state. + $DB = $this->createMock(get_class($DB)); + $DB->expects($this->atMost(1)) + ->method('record_exists') + ->willReturn($uservalue); + } + + $customcompletion = new custom_completion($mockcminfo, 2); + + $this->assertEquals($status, $customcompletion->get_state($rule)); + } + + /** + * Test for get_defined_custom_rules(). + */ + public function test_get_defined_custom_rules() { + $expectedrules = [ + 'completiontimespent', + 'completionendreached', + ]; + + $definedrules = custom_completion::get_defined_custom_rules(); + $this->assertCount(2, $definedrules); + + foreach ($definedrules as $definedrule) { + $this->assertContains($definedrule, $expectedrules); + } + } + + /** + * Test for get_defined_custom_rule_descriptions(). + */ + public function test_get_custom_rule_descriptions() { + // Get defined custom rules. + $rules = custom_completion::get_defined_custom_rules(); + + // Build a mock cm_info instance. + $mockcminfo = $this->getMockBuilder(cm_info::class) + ->disableOriginalConstructor() + ->onlyMethods(['__get']) + ->getMock(); + + // Instantiate a custom_completion object using the mocked cm_info. + $customcompletion = new custom_completion($mockcminfo, 1); + + // Get custom rule descriptions. + $ruledescriptions = $customcompletion->get_custom_rule_descriptions(); + + // Confirm that defined rules and rule descriptions are consistent with each other. + $this->assertEquals(count($rules), count($ruledescriptions)); + foreach ($rules as $rule) { + $this->assertArrayHasKey($rule, $ruledescriptions); + } + } + + /** + * Test for is_defined(). + */ + public function test_is_defined() { + // Build a mock cm_info instance. + $mockcminfo = $this->getMockBuilder(cm_info::class) + ->disableOriginalConstructor() + ->getMock(); + + $customcompletion = new custom_completion($mockcminfo, 1); + + // All rules are defined. + $this->assertTrue($customcompletion->is_defined('completiontimespent')); + $this->assertTrue($customcompletion->is_defined('completionendreached')); + + // Undefined rule is not found. + $this->assertFalse($customcompletion->is_defined('somerandomrule')); + } + + /** + * Data provider for test_get_available_custom_rules(). + * + * @return array[] + */ + public function get_available_custom_rules_provider(): array { + return [ + 'No completion conditions enabled' => [ + [ + 'completiontimespent' => COMPLETION_DISABLED, + 'completionendreached' => COMPLETION_DISABLED, + ], + [], + ], + 'Completion end reached enabled only' => [ + [ + 'completiontimespent' => COMPLETION_DISABLED, + 'completionendreached' => COMPLETION_ENABLED, + ], + ['completionendreached'], + ], + 'Completion time spent enabled only' => [ + [ + 'completiontimespent' => 60, + 'completionendreached' => COMPLETION_DISABLED, + ], + ['completiontimespent'], + ], + 'Completion end reached and time spent both enabled' => [ + [ + 'completiontimespent' => 90, + 'completionendreached' => COMPLETION_ENABLED, + ], + ['completiontimespent', 'completionendreached'], + ], + ]; + } + + /** + * Test for get_available_custom_rules(). + * + * @dataProvider get_available_custom_rules_provider + * @param array $completionrulesvalues + * @param array $expected + */ + public function test_get_available_custom_rules(array $completionrulesvalues, array $expected) { + $customcompletionrules = [ + 'customcompletionrules' => $completionrulesvalues, + ]; + + // Build a mock cm_info instance. + $mockcminfo = $this->getMockBuilder(cm_info::class) + ->disableOriginalConstructor() + ->onlyMethods(['__get']) + ->getMock(); + + // Mock the return of magic getter for the customdata attribute. + $mockcminfo->expects($this->any()) + ->method('__get') + ->with('customdata') + ->willReturn($customcompletionrules); + + $customcompletion = new custom_completion($mockcminfo, 1); + $this->assertEquals($expected, $customcompletion->get_available_custom_rules()); + } +}