From 8e031c47d6e30af6be051ca244914c3154ab7347 Mon Sep 17 00:00:00 2001 From: Dmitrii Metelkin Date: Wed, 3 Apr 2024 16:46:04 +1100 Subject: [PATCH 1/3] issue #41: add course completed condition --- classes/condition_form.php | 2 +- .../condition/course_completed.php | 244 ++++++++++++++++ lang/en/tool_dynamic_cohorts.php | 8 + .../condition/course_completed_test.php | 261 ++++++++++++++++++ version.php | 4 +- 5 files changed, 516 insertions(+), 3 deletions(-) create mode 100644 classes/local/tool_dynamic_cohorts/condition/course_completed.php create mode 100644 tests/local/tool_dynamic_cohorts/condition/course_completed_test.php diff --git a/classes/condition_form.php b/classes/condition_form.php index ad658a0..e9f7871 100644 --- a/classes/condition_form.php +++ b/classes/condition_form.php @@ -61,7 +61,7 @@ protected function get_condition(): condition_base { throw new coding_exception('Condition class name is not set'); } - $conditions = condition_manager::get_all_conditions(); + $conditions = condition_manager::get_all_conditions(false); if (!array_key_exists($this->_customdata['classname'], $conditions)) { throw new moodle_exception('Condition is broken. Invalid condition class.'); } diff --git a/classes/local/tool_dynamic_cohorts/condition/course_completed.php b/classes/local/tool_dynamic_cohorts/condition/course_completed.php new file mode 100644 index 0000000..333eb0d --- /dev/null +++ b/classes/local/tool_dynamic_cohorts/condition/course_completed.php @@ -0,0 +1,244 @@ +. + +namespace tool_dynamic_cohorts\local\tool_dynamic_cohorts\condition; + +use completion_info; +use tool_dynamic_cohorts\condition_base; +use tool_dynamic_cohorts\condition_sql; + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->libdir . '/completionlib.php'); + +/** + * Condition based on course completion. + * + * @package tool_dynamic_cohorts + * @copyright 2024 Catalyst IT + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_completed extends condition_base { + + /** + * Operator for any completion date. + */ + public const OPERATOR_ANY = 1; + + /** + * Operator for completion dates before some date. + */ + public const OPERATOR_BEFORE = 2; + + /** + * Operator for completion dates after some date. + */ + public const OPERATOR_AFTER = 3; + + /** + * Condition name. + * + * @return string + */ + public function get_name(): string { + return get_string('condition:course_completed', 'tool_dynamic_cohorts'); + } + + /** + * Gets a list of operators. + * + * @return array A list of operators. + */ + protected function get_operators(): array { + return [ + self::OPERATOR_ANY => get_string('any', 'tool_dynamic_cohorts'), + self::OPERATOR_BEFORE => get_string('before', 'tool_dynamic_cohorts'), + self::OPERATOR_AFTER => get_string('after', 'tool_dynamic_cohorts'), + ]; + } + + /** + * Add config form elements. + * + * @param \MoodleQuickForm $mform + */ + public function config_form_add(\MoodleQuickForm $mform): void { + $mform->addElement('course', 'courseid', get_string('course'), ['onlywithcompletion' => true]); + $mform->addRule('courseid', null, 'required', null, 'client'); + $mform->setType('courseid', PARAM_INT); + + $mform->addElement( + 'select', + 'operator', + get_string('completiondate', 'tool_dynamic_cohorts'), + $this->get_operators() + ); + + $mform->addElement('date_time_selector', 'timecompleted'); + $mform->hideIf('timecompleted', 'operator', 'eq', self::OPERATOR_ANY); + $mform->setDefault('timecompleted', usergetmidnight(time())); + } + + /** + * Validate config form elements. + * + * @param array $data Data to validate. + * @return array + */ + public function config_form_validate(array $data): array { + $errors = []; + + if (!isset($data['courseid'])) { + $errors['courseid'] = get_string('required'); + return $errors; + } + + return $errors; + } + + /** + * Gets configured course ID. + * + * @return int + */ + protected function get_courseid_value(): int { + return $this->get_config_data()['courseid'] ?? 0; + } + + /** + * Gets operator value. + * + * @return int + */ + protected function get_operator_value(): int { + return $this->get_config_data()['operator'] ?? self::OPERATOR_ANY; + } + + /** + * Gets configured completion time. + * + * @return int + */ + protected function get_timecompleted_value(): int { + return $this->get_config_data()['timecompleted'] ?? 0; + } + + /** + * Human-readable description of the configured condition. + * + * @return string + */ + public function get_config_description(): string { + global $DB; + + $coursename = $DB->get_field('course', 'fullname', ['id' => $this->get_courseid_value()]); + $coursename = format_string($coursename, true, ['context' => \context_system::instance(), 'escape' => false]); + + $operatorvalue = $this->get_operator_value(); + + $operator = $operatorvalue != self::OPERATOR_ANY ? strtolower($this->get_operators()[$operatorvalue]) : ''; + $timecompleted = $operatorvalue != self::OPERATOR_ANY ? userdate($this->get_timecompleted_value()) : ''; + + return get_string('condition:course_completed_description', 'tool_dynamic_cohorts', (object)[ + 'course' => $coursename, + 'operator' => $operator, + 'timecompleted' => $timecompleted, + ]); + } + + /** + * Human readable description of the broken condition. + * + * @return string + */ + public function get_broken_description(): string { + global $DB; + + // Check course exists. + if (!$course = $DB->get_record('course', ['id' => $this->get_courseid_value()])) { + return get_string('missingcourse', 'tool_dynamic_cohorts'); + } + + // Check completion is enabled for a course. + $completion = new completion_info($course); + if (!$completion->is_enabled()) { + return get_string('completionisdisabled', 'tool_dynamic_cohorts'); + } + + return parent::get_broken_description(); + } + + /** + * Gets SQL data for building SQL. + * + * @return condition_sql + */ + public function get_sql(): condition_sql { + $sql = new condition_sql('', '1=0', []); + + if (!$this->is_broken()) { + $params = []; + + $completiontable = condition_sql::generate_table_alias(); + $join = "JOIN {course_completions} $completiontable ON ($completiontable.userid = u.id)"; + + $courseid = $this->get_courseid_value(); + $courseidparam = condition_sql::generate_param_alias(); + $params[$courseidparam] = $courseid; + + $timecompleted = $this->get_timecompleted_value(); + $timecompletedparam = condition_sql::generate_param_alias(); + + $operator = $this->get_operator_value(); + + if ($operator != self::OPERATOR_ANY && $timecompleted > 0) { + $operator = $operator == self::OPERATOR_BEFORE ? '<' : '>'; + $where = "$completiontable.course = :$courseidparam + AND $completiontable.timecompleted $operator :$timecompletedparam"; + $params[$timecompletedparam] = $timecompleted; + } else { + $where = "$completiontable.course = :$courseidparam + AND $completiontable.timecompleted IS NOT NULL"; + } + + $sql = new condition_sql($join, $where, $params); + } + + return $sql; + } + + /** + * Is condition broken. + * + * @return bool + */ + public function is_broken(): bool { + global $DB; + + // Check course exists. + if (!$course = $DB->get_record('course', ['id' => $this->get_courseid_value()])) { + return true; + } + + // Check completion is enabled for a course. + $completion = new completion_info($course); + if (!$completion->is_enabled()) { + return true; + } + + return false; + } +} diff --git a/lang/en/tool_dynamic_cohorts.php b/lang/en/tool_dynamic_cohorts.php index 966e888..422090c 100644 --- a/lang/en/tool_dynamic_cohorts.php +++ b/lang/en/tool_dynamic_cohorts.php @@ -28,7 +28,10 @@ $string['addcondition'] = 'Add a condition'; $string['addrule'] = 'Add a new rule'; $string['add_rule'] = 'Add new rule'; +$string['after'] = 'After'; +$string['any'] = 'Any'; $string['backtolistofrules'] = 'Back to the list of rules'; +$string['before'] = 'Before'; $string['brokenruleswarning'] = 'There are some broken rules require your attention.
To fix a broken rule you should remove all broken conditions.
Sometimes a rule becomes broken when matching users SQL failed. In this case all condition are ok, but the rule is marked as broken. You should check Moodle logs for "Matching users failed" event and related SQL errors.
Please note, that in any case you have to re-save the rule to mark it as unbroken.'; $string['bulkprocessing'] = 'Bulk processing'; $string['bulkprocessing_help'] = 'If this option is enabled, users will be added and removed from cohort in bulk. This will significantly improve processing performance. However, using this option will suppress triggering events when users added or removed from cohort.'; @@ -38,6 +41,8 @@ $string['cohortid'] = 'Cohort'; $string['cohortswith'] = 'Cohort(s) with field'; $string['cohortid_help'] = 'A cohort to manage as part of this rule. Only cohorts that are not managed by other plugins are displayed in this list.'; +$string['completiondate'] = 'Completion date'; +$string['completionisdisabled'] = 'Completion is disabled for configured course'; $string['condition'] = 'Condition'; $string['conditions'] = 'Conditions'; $string['conditionstext'] = '{$a->conditions} ( logical {$a->operator} )'; @@ -50,6 +55,8 @@ $string['condition:cohort_membership_broken_description'] = 'Condition is broken. Using the same cohort that the given rule is configured to manage to.'; $string['condition:cohort_field'] = 'Cohort field'; $string['condition:cohort_field_description'] = 'A user {$a->operator} cohorts with field \'{$a->field}\' {$a->fieldoperator} {$a->fieldvalue}'; +$string['condition:course_completed'] = 'Course completed'; +$string['condition:course_completed_description'] = 'A user has completed course "{$a->course}" {$a->operator} {$a->timecompleted}'; $string['condition:profile_field_description'] = '{$a->field} {$a->fieldoperator} {$a->fieldvalue}'; $string['condition:user_profile'] = 'User standard profile field'; $string['condition:user_custom_profile'] = 'User custom profile field'; @@ -89,6 +96,7 @@ $string['managerules'] = 'Manage rules'; $string['managecohorts'] = 'Manage cohorts'; $string['matchingusers'] = 'Matching users'; +$string['missingcourse'] = 'Missing course'; $string['name'] = 'Rule name'; $string['name_help'] = 'A human readable name of this rule.'; $string['operator'] = 'Operator'; diff --git a/tests/local/tool_dynamic_cohorts/condition/course_completed_test.php b/tests/local/tool_dynamic_cohorts/condition/course_completed_test.php new file mode 100644 index 0000000..fd7279d --- /dev/null +++ b/tests/local/tool_dynamic_cohorts/condition/course_completed_test.php @@ -0,0 +1,261 @@ +. + +namespace tool_dynamic_cohorts\local\tool_dynamic_cohorts\condition; + +use tool_dynamic_cohorts\condition_base; +use tool_dynamic_cohorts\rule; + +/** + * Unit tests for course_completed condition class. + * + * @package tool_dynamic_cohorts + * @copyright 2024 Catalyst IT + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \tool_dynamic_cohorts\local\tool_dynamic_cohorts\condition\course_completed + */ +class course_completed_test extends \advanced_testcase { + + /** + * Get condition instance for testing. + * + * @param array $configdata Config data to be set. + * @return condition_base + */ + protected function get_condition(array $configdata = []): condition_base { + $condition = condition_base::get_instance(0, (object)[ + 'classname' => '\tool_dynamic_cohorts\local\tool_dynamic_cohorts\condition\course_completed', + ]); + $condition->set_config_data($configdata); + + return $condition; + } + + /** + * Test retrieving of config data. + */ + public function test_retrieving_configdata() { + $formdata = (object)[ + 'courseid' => 1, + 'operator' => 3, + 'timecompleted' => 777777, + 'ruleid' => 1, + 'sortorder' => 0, + ]; + + $actual = $this->get_condition()::retrieve_config_data($formdata); + $expected = [ + 'courseid' => 1, + 'operator' => 3, + 'timecompleted' => 777777, + ]; + $this->assertEquals($expected, $actual); + } + + /** + * Test setting and getting config data. + */ + public function test_set_and_get_configdata() { + $condition = $this->get_condition([ + 'courseid' => 1, + 'operator' => 3, + 'timecompleted' => 777777, + ]); + + $this->assertEquals( + [ + 'courseid' => 1, + 'operator' => 3, + 'timecompleted' => 777777, + ], + $condition->get_config_data() + ); + } + + /** + * Test getting config description. + */ + public function test_config_description() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $now = time(); + + $condition = $this->get_condition([ + 'courseid' => $course->id, + 'operator' => course_completed::OPERATOR_AFTER, + 'timecompleted' => $now, + ]); + + $this->assertSame( + 'A user has completed course "' . $course->fullname . '" after ' . userdate($now), + $condition->get_config_description(), + ); + + $condition = $this->get_condition([ + 'courseid' => $course->id, + 'operator' => course_completed::OPERATOR_BEFORE, + 'timecompleted' => $now, + ]); + + $this->assertSame( + 'A user has completed course "' . $course->fullname . '" before ' . userdate($now), + $condition->get_config_description(), + ); + + $condition = $this->get_condition([ + 'courseid' => $course->id, + 'operator' => course_completed::OPERATOR_ANY, + 'timecompleted' => $now, + ]); + + $this->assertSame( + 'A user has completed course "' . $course->fullname . '" ', + $condition->get_config_description(), + ); + } + + /** + * Test getting rule. + */ + public function test_get_rule() { + $this->resetAfterTest(); + + // Rule is not set. + $condition = $this->get_condition(); + $this->assertNull($condition->get_rule()); + + // Create a rule and set it to an instance. + $rule = new rule(0, (object)['name' => 'Test rule 1']); + $rule->save(); + + $condition = cohort_membership::get_instance(0, (object)['ruleid' => $rule->get('id')]); + $this->assertEquals($condition->get_rule()->get('id'), $rule->get('id')); + } + + /** + * Test is broken. + */ + public function test_is_broken_and_broken_description() { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + + // Invalid course. + $condition = $this->get_condition([ + 'courseid' => 7777, + 'operator' => course_completed::OPERATOR_ANY, + 'timecompleted' => 0, + ]); + $this->assertTrue($condition->is_broken()); + $this->assertSame('Missing course', $condition->get_broken_description()); + + // Completion is disabled. + $condition = $this->get_condition([ + 'courseid' => $course->id, + 'operator' => course_completed::OPERATOR_ANY, + 'timecompleted' => 0, + ]); + $this->assertTrue($condition->is_broken()); + $this->assertSame('Completion is disabled for configured course', $condition->get_broken_description()); + + // Completion is enabled. + $DB->set_field('course', 'enablecompletion', 1, ['id' => $course->id]); + $condition = $this->get_condition([ + 'courseid' => $course->id, + 'operator' => course_completed::OPERATOR_ANY, + 'timecompleted' => 0, + ]); + $this->assertFalse($condition->is_broken()); + } + + /** + * Test getting correct SQL. + */ + public function test_get_sql_data() { + global $DB; + + $this->resetAfterTest(); + + $now = time(); + + $studentrole = $DB->get_record('role', ['shortname' => 'student']); + $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + $this->getDataGenerator()->enrol_user($user1->id, $course->id, $studentrole->id); + $this->getDataGenerator()->enrol_user($user2->id, $course->id, $studentrole->id); + + $completionuser1 = new \completion_completion(['userid' => $user1->id, 'course' => $course->id]); + $completionuser1->mark_complete($now + WEEKSECS); + + $completionuser2 = new \completion_completion(['userid' => $user2->id, 'course' => $course->id]); + $completionuser2->mark_complete($now - WEEKSECS); + + $totalusers = $DB->count_records('user'); + $this->assertTrue($totalusers > 2); + + // Any completion. Should get user 1 and user 2. + $condition = $this->get_condition([ + 'courseid' => $course->id, + 'operator' => course_completed::OPERATOR_ANY, + 'timecompleted' => 0, + ]); + + $result = $condition->get_sql(); + $sql = "SELECT u.id FROM {user} u {$result->get_join()} WHERE {$result->get_where()}"; + $this->assertCount(2, $DB->get_records_sql($sql, $result->get_params())); + + // Before. Should get user 2. + $condition = $this->get_condition([ + 'courseid' => $course->id, + 'operator' => course_completed::OPERATOR_BEFORE, + 'timecompleted' => $now, + ]); + + $result = $condition->get_sql(); + $sql = "SELECT u.id FROM {user} u {$result->get_join()} WHERE {$result->get_where()}"; + $actual = $DB->get_records_sql($sql, $result->get_params()); + $this->assertCount(1, $actual); + $this->assertSame($user2->id, reset($actual)->id); + + // After. Should get user 1. + $condition = $this->get_condition([ + 'courseid' => $course->id, + 'operator' => course_completed::OPERATOR_AFTER, + 'timecompleted' => $now, + ]); + + $result = $condition->get_sql(); + $sql = "SELECT u.id FROM {user} u {$result->get_join()} WHERE {$result->get_where()}"; + $actual = $DB->get_records_sql($sql, $result->get_params()); + $this->assertCount(1, $actual); + $this->assertSame($user1->id, reset($actual)->id); + } + + /** + * Test events that the condition is listening to. + */ + public function test_get_events() { + $this->assertEquals([], $this->get_condition()->get_events()); + } + +} diff --git a/version.php b/version.php index d6388c0..027b861 100644 --- a/version.php +++ b/version.php @@ -25,8 +25,8 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'tool_dynamic_cohorts'; -$plugin->release = 2024040200; -$plugin->version = 2024040200; +$plugin->release = 2024040300; +$plugin->version = 2024040300; $plugin->requires = 2022112800; $plugin->supported = [401, 403]; $plugin->maturity = MATURITY_STABLE; From 61fe9f426491e87cecf0b5adf11aa85ce3ef1d48 Mon Sep 17 00:00:00 2001 From: Dmitrii Metelkin Date: Thu, 4 Apr 2024 10:23:11 +1100 Subject: [PATCH 2/3] issue #41: fix an issue when not configured condition was broken --- .../condition/course_completed.php | 18 ++++++++++-------- .../condition/course_completed_test.php | 7 ++++++- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/classes/local/tool_dynamic_cohorts/condition/course_completed.php b/classes/local/tool_dynamic_cohorts/condition/course_completed.php index 333eb0d..a9f6243 100644 --- a/classes/local/tool_dynamic_cohorts/condition/course_completed.php +++ b/classes/local/tool_dynamic_cohorts/condition/course_completed.php @@ -228,15 +228,17 @@ public function get_sql(): condition_sql { public function is_broken(): bool { global $DB; - // Check course exists. - if (!$course = $DB->get_record('course', ['id' => $this->get_courseid_value()])) { - return true; - } + if ($this->get_config_data()) { + // Check course exists. + if (!$course = $DB->get_record('course', ['id' => $this->get_courseid_value()])) { + return true; + } - // Check completion is enabled for a course. - $completion = new completion_info($course); - if (!$completion->is_enabled()) { - return true; + // Check completion is enabled for a course. + $completion = new completion_info($course); + if (!$completion->is_enabled()) { + return true; + } } return false; diff --git a/tests/local/tool_dynamic_cohorts/condition/course_completed_test.php b/tests/local/tool_dynamic_cohorts/condition/course_completed_test.php index fd7279d..3c5bbf5 100644 --- a/tests/local/tool_dynamic_cohorts/condition/course_completed_test.php +++ b/tests/local/tool_dynamic_cohorts/condition/course_completed_test.php @@ -157,6 +157,12 @@ public function test_is_broken_and_broken_description() { $course = $this->getDataGenerator()->create_course(); + $condition = condition_base::get_instance(0, (object)[ + 'classname' => '\tool_dynamic_cohorts\local\tool_dynamic_cohorts\condition\course_completed', + ]); + + $this->assertFalse($condition->is_broken()); + // Invalid course. $condition = $this->get_condition([ 'courseid' => 7777, @@ -257,5 +263,4 @@ public function test_get_sql_data() { public function test_get_events() { $this->assertEquals([], $this->get_condition()->get_events()); } - } From bac5a2494d7fd343c98d2b8e317b83d55a7319e1 Mon Sep 17 00:00:00 2001 From: Dmitrii Metelkin Date: Thu, 4 Apr 2024 10:43:39 +1100 Subject: [PATCH 3/3] issue #41: add course not completed condition --- .../condition/course_not_completed.php | 93 ++++++++ lang/en/tool_dynamic_cohorts.php | 2 + .../condition/course_not_completed_test.php | 206 ++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 classes/local/tool_dynamic_cohorts/condition/course_not_completed.php create mode 100644 tests/local/tool_dynamic_cohorts/condition/course_not_completed_test.php diff --git a/classes/local/tool_dynamic_cohorts/condition/course_not_completed.php b/classes/local/tool_dynamic_cohorts/condition/course_not_completed.php new file mode 100644 index 0000000..46672fd --- /dev/null +++ b/classes/local/tool_dynamic_cohorts/condition/course_not_completed.php @@ -0,0 +1,93 @@ +. + +namespace tool_dynamic_cohorts\local\tool_dynamic_cohorts\condition; + +use tool_dynamic_cohorts\condition_sql; + +/** + * Condition based on course completion. + * + * @package tool_dynamic_cohorts + * @copyright 2024 Catalyst IT + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_not_completed extends course_completed { + + /** + * Condition name. + * + * @return string + */ + public function get_name(): string { + return get_string('condition:course_not_completed', 'tool_dynamic_cohorts'); + } + + /** + * Add config form elements. + * + * @param \MoodleQuickForm $mform + */ + public function config_form_add(\MoodleQuickForm $mform): void { + $mform->addElement('course', 'courseid', get_string('course'), ['onlywithcompletion' => true]); + $mform->addRule('courseid', null, 'required', null, 'client'); + $mform->setType('courseid', PARAM_INT); + } + + /** + * Human-readable description of the configured condition. + * + * @return string + */ + public function get_config_description(): string { + global $DB; + + $coursename = $DB->get_field('course', 'fullname', ['id' => $this->get_courseid_value()]); + $coursename = format_string($coursename, true, ['context' => \context_system::instance(), 'escape' => false]); + + return get_string('condition:course_not_completed_description', 'tool_dynamic_cohorts', (object)[ + 'course' => $coursename, + ]); + } + + /** + * Gets SQL data for building SQL. + * + * @return condition_sql + */ + public function get_sql(): condition_sql { + $sql = new condition_sql('', '1=0', []); + + if (!$this->is_broken()) { + $params = []; + + $completiontable = condition_sql::generate_table_alias(); + + $courseid = $this->get_courseid_value(); + $courseidparam = condition_sql::generate_param_alias(); + $params[$courseidparam] = $courseid; + + $join = "LEFT JOIN {course_completions} $completiontable + ON ($completiontable.userid = u.id AND $completiontable.course = :$courseidparam)"; + + $where = "$completiontable.timecompleted IS NULL OR $completiontable.id IS NULL"; + + $sql = new condition_sql($join, $where, $params); + } + + return $sql; + } +} diff --git a/lang/en/tool_dynamic_cohorts.php b/lang/en/tool_dynamic_cohorts.php index 422090c..74a9612 100644 --- a/lang/en/tool_dynamic_cohorts.php +++ b/lang/en/tool_dynamic_cohorts.php @@ -57,6 +57,8 @@ $string['condition:cohort_field_description'] = 'A user {$a->operator} cohorts with field \'{$a->field}\' {$a->fieldoperator} {$a->fieldvalue}'; $string['condition:course_completed'] = 'Course completed'; $string['condition:course_completed_description'] = 'A user has completed course "{$a->course}" {$a->operator} {$a->timecompleted}'; +$string['condition:course_not_completed'] = 'Course not completed'; +$string['condition:course_not_completed_description'] = 'A user has not completed course "{$a->course}"'; $string['condition:profile_field_description'] = '{$a->field} {$a->fieldoperator} {$a->fieldvalue}'; $string['condition:user_profile'] = 'User standard profile field'; $string['condition:user_custom_profile'] = 'User custom profile field'; diff --git a/tests/local/tool_dynamic_cohorts/condition/course_not_completed_test.php b/tests/local/tool_dynamic_cohorts/condition/course_not_completed_test.php new file mode 100644 index 0000000..7a95246 --- /dev/null +++ b/tests/local/tool_dynamic_cohorts/condition/course_not_completed_test.php @@ -0,0 +1,206 @@ +. + +namespace tool_dynamic_cohorts\local\tool_dynamic_cohorts\condition; + +use tool_dynamic_cohorts\condition_base; +use tool_dynamic_cohorts\rule; + +/** + * Unit tests for course_not_completed condition class. + * + * @package tool_dynamic_cohorts + * @copyright 2024 Catalyst IT + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @covers \tool_dynamic_cohorts\local\tool_dynamic_cohorts\condition\course_not_completed + */ +class course_not_completed_test extends \advanced_testcase { + + /** + * Get condition instance for testing. + * + * @param array $configdata Config data to be set. + * @return condition_base + */ + protected function get_condition(array $configdata = []): condition_base { + $condition = condition_base::get_instance(0, (object)[ + 'classname' => '\tool_dynamic_cohorts\local\tool_dynamic_cohorts\condition\course_not_completed', + ]); + $condition->set_config_data($configdata); + + return $condition; + } + + /** + * Test retrieving of config data. + */ + public function test_retrieving_configdata() { + $formdata = (object)[ + 'courseid' => 1, + 'ruleid' => 1, + 'sortorder' => 0, + ]; + + $actual = $this->get_condition()::retrieve_config_data($formdata); + $expected = [ + 'courseid' => 1, + ]; + $this->assertEquals($expected, $actual); + } + + /** + * Test setting and getting config data. + */ + public function test_set_and_get_configdata() { + $condition = $this->get_condition([ + 'courseid' => 1, + ]); + + $this->assertEquals( + [ + 'courseid' => 1, + ], + $condition->get_config_data() + ); + } + + /** + * Test getting config description. + */ + public function test_config_description() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + + $condition = $this->get_condition([ + 'courseid' => $course->id, + ]); + + $this->assertSame( + 'A user has not completed course "' . $course->fullname . '"', + $condition->get_config_description(), + ); + } + + /** + * Test getting rule. + */ + public function test_get_rule() { + $this->resetAfterTest(); + + // Rule is not set. + $condition = $this->get_condition(); + $this->assertNull($condition->get_rule()); + + // Create a rule and set it to an instance. + $rule = new rule(0, (object)['name' => 'Test rule 1']); + $rule->save(); + + $condition = cohort_membership::get_instance(0, (object)['ruleid' => $rule->get('id')]); + $this->assertEquals($condition->get_rule()->get('id'), $rule->get('id')); + } + + /** + * Test is broken. + */ + public function test_is_broken_and_broken_description() { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + + $condition = condition_base::get_instance(0, (object)[ + 'classname' => '\tool_dynamic_cohorts\local\tool_dynamic_cohorts\condition\course_not_completed', + ]); + + $this->assertFalse($condition->is_broken()); + + // Invalid course. + $condition = $this->get_condition([ + 'courseid' => 7777, + ]); + $this->assertTrue($condition->is_broken()); + $this->assertSame('Missing course', $condition->get_broken_description()); + + // Completion is disabled. + $condition = $this->get_condition([ + 'courseid' => $course->id, + ]); + $this->assertTrue($condition->is_broken()); + $this->assertSame('Completion is disabled for configured course', $condition->get_broken_description()); + + // Completion is enabled. + $DB->set_field('course', 'enablecompletion', 1, ['id' => $course->id]); + $condition = $this->get_condition([ + 'courseid' => $course->id, + ]); + $this->assertFalse($condition->is_broken()); + } + + /** + * Test getting correct SQL. + */ + public function test_get_sql_data() { + global $DB; + + $this->resetAfterTest(); + + $now = time(); + + $studentrole = $DB->get_record('role', ['shortname' => 'student']); + $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); + + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $user3 = $this->getDataGenerator()->create_user(); + $user4 = $this->getDataGenerator()->create_user(); + + $this->getDataGenerator()->enrol_user($user1->id, $course->id, $studentrole->id); + $this->getDataGenerator()->enrol_user($user2->id, $course->id, $studentrole->id); + + $completionuser1 = new \completion_completion(['userid' => $user1->id, 'course' => $course->id]); + $completionuser1->mark_complete($now + WEEKSECS); + + $completionuser2 = new \completion_completion(['userid' => $user2->id, 'course' => $course->id]); + $completionuser2->mark_complete($now - WEEKSECS); + + $totalusers = $DB->count_records('user'); + $this->assertTrue($totalusers > 4); + + // Should get all users except user 1 and user 2. + $condition = $this->get_condition([ + 'courseid' => $course->id, + ]); + + $result = $condition->get_sql(); + $sql = "SELECT u.id FROM {user} u {$result->get_join()} WHERE {$result->get_where()}"; + $this->assertCount($totalusers - 2, $DB->get_records_sql($sql, $result->get_params())); + $actual = $DB->get_records_sql($sql, $result->get_params()); + $this->assertArrayNotHasKey($user1->id, $actual); + $this->assertArrayNotHasKey($user2->id, $actual); + $this->assertArrayHasKey($user3->id, $actual); + $this->assertArrayHasKey($user4->id, $actual); + } + + /** + * Test events that the condition is listening to. + */ + public function test_get_events() { + $this->assertEquals([], $this->get_condition()->get_events()); + } +}