diff --git a/classes/assessment/assessment.php b/classes/assessment/assessment.php index afbe366..62cb975 100644 --- a/classes/assessment/assessment.php +++ b/classes/assessment/assessment.php @@ -75,6 +75,15 @@ public function apply_extension(extension $extension): void { if ($extension instanceof ec) { $this->apply_ec_extension($extension); } else if ($extension instanceof sora) { + // Skip SORA overrides if the assessment is not an exam. + if (!$this->is_exam()) { + return; + } + // Skip SORA overrides if the end date of the assessment is in the past. + if ($this->get_end_date() < time()) { + return; + } + $this->apply_sora_extension($extension); } } @@ -190,6 +199,24 @@ public function get_end_date(): ?int { return null; } + /** + * Get module name. Return empty string if not applicable. + * + * @return string + */ + public function get_module_name(): string { + return ''; + } + + /** + * Check if the assessment is an exam. Override in child class if needed. + * + * @return bool + */ + public function is_exam(): bool { + return false; + } + /** * Check if the assessment is valid for marks transfer. * @@ -236,6 +263,18 @@ public function is_valid_for_extension(): \stdClass { return $this->set_validity_result(true); } + /** + * Delete SORA override for a Moodle assessment. + * + * @param array $groupids Default SORA overrides group ids in the course. + * @return void + * @throws \moodle_exception + */ + public function delete_sora_overrides(array $groupids): void { + // Default not supported. Override in child class if needed. + throw new \moodle_exception('error:soraextensionnotsupported', 'local_sitsgradepush'); + } + /** * Set validity result. * diff --git a/classes/assessment/assign.php b/classes/assessment/assign.php index cabf6a6..779a43b 100644 --- a/classes/assessment/assign.php +++ b/classes/assessment/assign.php @@ -67,6 +67,57 @@ public function get_end_date(): ?int { return $this->sourceinstance->duedate; } + /** + * Check if this assignment is an exam. + * + * @return bool + */ + public function is_exam(): bool { + $start = $this->get_start_date(); + $end = $this->get_end_date(); + + if ($start && $end) { + $duration = $end - $start; + return $duration > 0 && $duration <= HOURSECS * 5; + } + + return false; + } + + /** + * Delete SORA override for the assignment. + * + * @param array $groupids Default SORA overrides group ids in the course. + * @return void + * @throws \coding_exception + * @throws \dml_exception + */ + public function delete_sora_overrides(array $groupids): void { + global $CFG, $DB; + require_once($CFG->dirroot . '/mod/assign/locallib.php'); + + // Skip if group ids are empty. + if (empty($groupids)) { + return; + } + + // Find all group overrides for the assignment having the default SORA overrides group ids. + [$insql, $params] = $DB->get_in_or_equal($groupids, SQL_PARAMS_NAMED); + $params['assignid'] = $this->sourceinstance->id; + $sql = "SELECT id FROM {assign_overrides} WHERE assignid = :assignid AND groupid $insql AND userid IS NULL"; + + $overrides = $DB->get_records_sql($sql, $params); + + if (empty($overrides)) { + return; + } + + $assign = new \assign($this->context, $this->get_course_module(), null); + foreach ($overrides as $override) { + $assign->delete_override($override->id); + } + } + /** * Apply EC extension to the assessment. * diff --git a/classes/assessment/quiz.php b/classes/assessment/quiz.php index aa84c53..1fc94ff 100644 --- a/classes/assessment/quiz.php +++ b/classes/assessment/quiz.php @@ -67,6 +67,48 @@ public function get_end_date(): ?int { return $this->get_source_instance()->timeclose; } + /** + * Check if this quiz is an exam. + * + * @return bool + */ + public function is_exam(): bool { + $originaltimelimit = $this->get_source_instance()->timelimit; + return $originaltimelimit > 0 && $originaltimelimit <= HOURMINS * 5; + } + + /** + * Delete SORA override for the quiz. + * + * @param array $groupids Default SORA overrides group ids in the course. + * @return void + * @throws \coding_exception + * @throws \dml_exception + */ + public function delete_sora_overrides(array $groupids): void { + global $CFG, $DB; + require_once($CFG->dirroot . '/mod/assign/locallib.php'); + + // Skip if group ids are empty. + if (empty($groupids)) { + return; + } + + // Find all group overrides for the quiz having the default SORA overrides group ids. + [$insql, $params] = $DB->get_in_or_equal($groupids, SQL_PARAMS_NAMED); + $params['quizid'] = $this->sourceinstance->id; + $sql = "SELECT * FROM {quiz_overrides} WHERE quiz = :quizid AND groupid $insql AND userid IS NULL"; + + $overrides = $DB->get_records_sql($sql, $params); + + if (empty($overrides)) { + return; + } + + // Delete the overrides. + $this->get_override_manager()->delete_overrides($overrides); + } + /** * Apply EC extension to the quiz. * diff --git a/classes/event/assessment_mapped.php b/classes/event/assessment_mapped.php index 8f3916f..bb394e8 100644 --- a/classes/event/assessment_mapped.php +++ b/classes/event/assessment_mapped.php @@ -43,7 +43,7 @@ protected function init(): void { * @throws \coding_exception */ public static function get_name(): string { - return get_string('eventname', 'local_sitsgradepush'); + return get_string('event:assessment_mapped', 'local_sitsgradepush'); } /** @@ -52,6 +52,6 @@ public static function get_name(): string { * @return string */ public function get_description(): string { - return "An assessment is mapped to a SITS assessment component."; + return get_string('event:assessment_mapped_desc', 'local_sitsgradepush'); } } diff --git a/classes/extensionmanager.php b/classes/extensionmanager.php index d775f3c..7ff3bfb 100644 --- a/classes/extensionmanager.php +++ b/classes/extensionmanager.php @@ -16,6 +16,9 @@ namespace local_sitsgradepush; +use local_sitsgradepush\assessment\assessment; +use local_sitsgradepush\assessment\assessmentfactory; +use local_sitsgradepush\extension\extension; use local_sitsgradepush\extension\sora; use local_sitsgradepush\task\process_extensions_new_enrolment; @@ -105,4 +108,46 @@ public static function get_user_enrolment_events(int $courseid): array { limitnum: process_extensions_new_enrolment::BATCH_LIMIT ); } + + /** + * Delete SORA overrides for a Moodle assessment. + * + * @param \stdClass $deletedmapping + * @return void + * @throws \dml_exception + */ + public static function delete_sora_overrides(\stdClass $deletedmapping): void { + try { + // Get Moodle assessment. + $assessment = assessmentfactory::get_assessment($deletedmapping->sourcetype, $deletedmapping->sourceid); + + // Nothing to do if the module type is not supported. + if (!extension::is_module_supported($assessment->get_module_name())) { + return; + } + + $assessment->delete_sora_overrides(self::get_default_sora_groups_ids_in_course($deletedmapping->courseid)); + } catch (\Exception $e) { + logger::log($e->getMessage(), null, "Deleted Mapping: " . json_encode($deletedmapping)); + } + } + + /** + * Get the default SORA groups IDs in a course. + * + * @param int $courseid + * @return array + * @throws \dml_exception + */ + public static function get_default_sora_groups_ids_in_course(int $courseid): array { + global $DB; + $like = $DB->sql_like('name', ':name', false); + $defaultsoragroups = $DB->get_records_select( + 'groups', + "courseid = :courseid AND $like", + ['courseid' => $courseid, 'name' => sora::SORA_GROUP_PREFIX . '%'], + fields: 'id', + ); + return !empty($defaultsoragroups) ? array_keys($defaultsoragroups) : []; + } } diff --git a/classes/manager.php b/classes/manager.php index 829dbf7..e83918b 100644 --- a/classes/manager.php +++ b/classes/manager.php @@ -474,6 +474,9 @@ public function save_assessment_mapping(\stdClass $data): int|bool { // Checked in the above validation, the current mapping to this component grade // can be deleted as it does not have push records nor mapped to the current activity. $DB->delete_records(self::TABLE_ASSESSMENT_MAPPING, ['id' => $existingmapping->id]); + + // Delete any SORA overrides for the deleted mapping. + extensionmanager::delete_sora_overrides($existingmapping); assesstype::update_assess_type($existingmapping, assesstype::ACTION_UNLOCK); } @@ -1520,7 +1523,7 @@ public function remove_mapping(int $courseid, int $mappingid): void { } // Check the mapping exists. - if (!$DB->record_exists(self::TABLE_ASSESSMENT_MAPPING, ['id' => $mappingid])) { + if (!$mapping = $DB->get_record(self::TABLE_ASSESSMENT_MAPPING, ['id' => $mappingid])) { throw new \moodle_exception('error:assessmentmapping', 'local_sitsgradepush', '', $mappingid); } @@ -1531,6 +1534,9 @@ public function remove_mapping(int $courseid, int $mappingid): void { // Everything is fine, remove the mapping. $DB->delete_records(self::TABLE_ASSESSMENT_MAPPING, ['id' => $mappingid]); + + // Delete any SORA overrides for the deleted mapping. + extensionmanager::delete_sora_overrides($mapping); } /** diff --git a/lang/en/local_sitsgradepush.php b/lang/en/local_sitsgradepush.php index 3139f48..321a255 100644 --- a/lang/en/local_sitsgradepush.php +++ b/lang/en/local_sitsgradepush.php @@ -144,6 +144,7 @@ $string['error:turnitin_numparts'] = 'Turnitin assignment with multiple parts is not supported by Marks Transfer.'; $string['error:user_data_not_set.'] = 'User data is not set.'; $string['event:assessment_mapped'] = 'Assessment mapped'; +$string['event:assessment_mapped_desc'] = 'An assessment is mapped to a SITS assessment component.'; $string['form:alert_no_mab_found'] = 'No assessment components found'; $string['form:info_turnitin_numparts'] = 'Please note Turnitin assignment with multiple parts is not supported by Marks Transfer.'; $string['gradepushassessmentselect'] = 'Select SITS assessment'; diff --git a/tests/aws/sqs_test.php b/tests/aws/sqs_test.php new file mode 100644 index 0000000..f0077a6 --- /dev/null +++ b/tests/aws/sqs_test.php @@ -0,0 +1,95 @@ +. + +namespace local_sitsgradepush; + +use Aws\Sqs\SqsClient; +use local_sitsgradepush\aws\sqs; + +/** + * Tests for the sqs class. + * + * @package local_sitsgradepush + * @copyright 2024 onwards University College London {@link https://www.ucl.ac.uk/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Alex Yeung + */ +final class sqs_test extends \advanced_testcase { + + /** + * Set up the test. + * + * @return void + */ + protected function setUp(): void { + $this->resetAfterTest(); + } + + /** + * Test get_client returns sqs client. + * + * @covers \local_sitsgradepush\aws\sqs::get_client + * @return void + */ + public function test_get_client_returns_sqs_client(): void { + $sqs = new sqs(); + $client = $sqs->get_client(); + $this->assertInstanceOf(SqsClient::class, $client); + } + + /** + * Test constructor throws exception if configs missing. + * + * @covers \local_sitsgradepush\aws\sqs::__construct + * @return void + * @throws \coding_exception + */ + public function test_constructor_throws_exception_if_configs_missing(): void { + // Set the required configs to empty. + set_config('aws_region', '', 'local_sitsgradepush'); + set_config('aws_key', '', 'local_sitsgradepush'); + set_config('aws_secret', '', 'local_sitsgradepush'); + + $this->expectException(\moodle_exception::class); + $this->expectExceptionMessage(get_string('error:missingrequiredconfigs', 'local_sitsgradepush')); + + new sqs(); + } + + /** + * Test check_required_configs_are_set returns configs. + * + * @covers \local_sitsgradepush\aws\sqs::__construct + * @covers \local_sitsgradepush\aws\sqs::check_required_configs_are_set + * @return void + * @throws \ReflectionException + */ + public function test_check_required_configs_are_set_returns_configs(): void { + // Set the required configs. + set_config('aws_region', 'us-east', 'local_sitsgradepush'); + set_config('aws_key', 'awskey-1234', 'local_sitsgradepush'); + set_config('aws_secret', 'secret-2468', 'local_sitsgradepush'); + + $sqs = new sqs(); + $reflection = new \ReflectionClass($sqs); + $method = $reflection->getMethod('check_required_configs_are_set'); + $method->setAccessible(true); + $configs = $method->invoke($sqs); + $this->assertEquals('us-east', $configs->aws_region); + $this->assertEquals('awskey-1234', $configs->aws_key); + $this->assertEquals('secret-2468', $configs->aws_secret); + } +} diff --git a/tests/extension/extension_common.php b/tests/extension/extension_common.php new file mode 100644 index 0000000..12821a4 --- /dev/null +++ b/tests/extension/extension_common.php @@ -0,0 +1,147 @@ +. + +namespace local_sitsgradepush; + +defined('MOODLE_INTERNAL') || die(); +global $CFG; +require_once($CFG->dirroot . '/local/sitsgradepush/tests/fixtures/tests_data_provider.php'); +require_once($CFG->dirroot . '/local/sitsgradepush/tests/base_test_class.php'); + +/** + * Base class for extension tests. + * + * @package local_sitsgradepush + * @copyright 2024 onwards University College London {@link https://www.ucl.ac.uk/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Alex Yeung + */ +class extension_common extends base_test_class { + + /** @var \stdClass $course1 Default test course 1 */ + protected \stdClass $course1; + + /** @var \stdClass Default test student 1 */ + protected \stdClass $student1; + + /** @var \stdClass Default test assignment 1 */ + protected \stdClass $assign1; + + /** @var \stdClass Default test quiz 1*/ + protected \stdClass $quiz1; + + /** + * Set up the test. + * + * @return void + */ + protected function setUp(): void { + parent::setUp(); + $this->resetAfterTest(); + + // Set admin user. + $this->setAdminUser(); + + // Get data generator. + $dg = $this->getDataGenerator(); + + // Set Easikit API client. + set_config('apiclient', 'easikit', 'local_sitsgradepush'); + + // Setup testing environment. + set_config('late_summer_assessment_end_' . date('Y'), date('Y-m-d', strtotime('+2 month')), 'block_lifecycle'); + + // Enable the extension. + set_config('extension_enabled', '1', 'local_sitsgradepush'); + + // Create a custom category and custom field. + $dg->create_custom_field_category(['name' => 'CLC']); + $dg->create_custom_field(['category' => 'CLC', 'shortname' => 'course_year']); + + // Create test courses. + $this->course1 = $dg->create_course( + ['shortname' => 'C1', 'customfields' => [ + ['shortname' => 'course_year', 'value' => date('Y')], + ]]); + $this->student1 = $dg->create_user(['idnumber' => '12345678']); + $dg->enrol_user($this->student1->id, $this->course1->id); + + $assessmentstartdate = strtotime('+1 hour'); + $assessmentenddate = strtotime('+3 hours'); + + // Create test assignment 1. + $this->assign1 = $dg->create_module('assign', + [ + 'name' => 'Test Assignment 1', + 'course' => $this->course1->id, + 'allowsubmissionsfromdate' => $assessmentstartdate, + 'duedate' => $assessmentenddate, + ] + ); + + // Create test quiz 1. + $this->quiz1 = $dg->create_module( + 'quiz', + [ + 'course' => $this->course1->id, + 'name' => 'Test Quiz 1', + 'timeopen' => $assessmentstartdate, + 'timelimit' => 60, + 'timeclose' => $assessmentenddate, + ] + ); + + // Set up the SITS grade push. + $this->setup_sitsgradepush(); + } + + /** + * Set up the SITS grade push. + * + * @return void + * @throws \dml_exception|\coding_exception + */ + protected function setup_sitsgradepush(): void { + // Insert MABs. + tests_data_provider::import_sitsgradepush_grade_components(); + } + + /** + * Insert a test mapping. + * + * @param int $mabid + * @param int $courseid + * @param \stdClass $assessment + * @param string $modtype + * @return bool|int + * @throws \dml_exception + */ + protected function insert_mapping(int $mabid, int $courseid, \stdClass $assessment, string $modtype): bool|int { + global $DB; + + return $DB->insert_record('local_sitsgradepush_mapping', [ + 'courseid' => $courseid, + 'sourceid' => $assessment->cmid, + 'sourcetype' => 'mod', + 'moduletype' => $modtype, + 'componentgradeid' => $mabid, + 'reassessment' => 0, + 'enableextension' => extensionmanager::is_extension_enabled() ? 1 : 0, + 'timecreated' => time(), + 'timemodified' => time(), + ]); + } +} diff --git a/tests/extension/extension_test.php b/tests/extension/extension_test.php index e893a3f..d7b972a 100644 --- a/tests/extension/extension_test.php +++ b/tests/extension/extension_test.php @@ -17,12 +17,11 @@ namespace local_sitsgradepush; use local_sitsgradepush\extension\ec; -use local_sitsgradepush\extension\sora; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/local/sitsgradepush/tests/fixtures/tests_data_provider.php'); -require_once($CFG->dirroot . '/local/sitsgradepush/tests/base_test_class.php'); +require_once($CFG->dirroot . '/local/sitsgradepush/tests/extension/extension_common.php'); /** * Tests for the extension class. @@ -32,84 +31,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Alex Yeung */ -final class extension_test extends base_test_class { - - /** @var \stdClass $course1 Default test course 1 */ - private \stdClass $course1; - - /** @var \stdClass Default test student 1 */ - private \stdClass $student1; - - /** @var \stdClass Default test assignment 1 */ - private \stdClass $assign1; - - /** @var \stdClass Default test quiz 1*/ - private \stdClass $quiz1; - - /** - * Set up the test. - * - * @return void - */ - protected function setUp(): void { - parent::setUp(); - $this->resetAfterTest(); - - // Set admin user. - $this->setAdminUser(); - - // Get data generator. - $dg = $this->getDataGenerator(); - - // Set Easikit API client. - set_config('apiclient', 'easikit', 'local_sitsgradepush'); - - // Setup testing environment. - set_config('late_summer_assessment_end_' . date('Y'), date('Y-m-d', strtotime('+2 month')), 'block_lifecycle'); - - // Enable the extension. - set_config('extension_enabled', '1', 'local_sitsgradepush'); - - // Create a custom category and custom field. - $dg->create_custom_field_category(['name' => 'CLC']); - $dg->create_custom_field(['category' => 'CLC', 'shortname' => 'course_year']); - - // Create test courses. - $this->course1 = $dg->create_course( - ['shortname' => 'C1', 'customfields' => [ - ['shortname' => 'course_year', 'value' => date('Y')], - ]]); - $this->student1 = $dg->create_user(['idnumber' => '12345678']); - $dg->enrol_user($this->student1->id, $this->course1->id); - - $assessmentstartdate = strtotime('+1 day'); - $assessmentenddate = strtotime('+2 days'); - - // Create test assignment 1. - $this->assign1 = $dg->create_module('assign', - [ - 'name' => 'Test Assignment 1', - 'course' => $this->course1->id, - 'allowsubmissionsfromdate' => $assessmentstartdate, - 'duedate' => $assessmentenddate, - ] - ); - - // Create test quiz 1. - $this->quiz1 = $dg->create_module( - 'quiz', - [ - 'course' => $this->course1->id, - 'name' => 'Test Quiz 1', - 'timeopen' => $assessmentstartdate, - 'timelimit' => 60, - 'timeclose' => $assessmentenddate, - ] - ); - - // Set up the SITS grade push. - $this->setup_sitsgradepush(); - } +final class extension_test extends extension_common { /** * Test no overrides for mapping without extension enabled. @@ -207,121 +129,6 @@ public function test_ec_process_extension_quiz(): void { $this->assertEquals($newtimeclose, $override->timeclose); } - /** - * Test SORA extension process. - * - * @covers \local_sitsgradepush\extension\extension::get_mappings_by_userid - * @covers \local_sitsgradepush\extension\sora::process_extension - * @covers \local_sitsgradepush\extension\sora::get_sora_group_id - * @covers \local_sitsgradepush\assessment\assign::apply_sora_extension - * @covers \local_sitsgradepush\assessment\quiz::apply_sora_extension - * @throws \coding_exception - * @throws \dml_exception - * @throws \moodle_exception - */ - public function test_sora_process_extension(): void { - global $DB; - - // Insert mappings for SORA. - $this->setup_for_sora_testing(); - - // Process the extension by passing the JSON event data. - $sora = new sora(); - $sora->set_properties_from_aws_message(tests_data_provider::get_sora_event_data()); - $sora->process_extension($sora->get_mappings_by_userid($sora->get_userid())); - - // Test SORA override group exists. - $groupid = $DB->get_field('groups', 'id', ['name' => $sora->get_extension_group_name()]); - $this->assertNotEmpty($groupid); - - // Test user is added to the SORA group. - $groupmember = $DB->get_record('groups_members', ['groupid' => $groupid, 'userid' => $this->student1->id]); - $this->assertNotEmpty($groupmember); - - // Test group override set in the assignment. - $override = $DB - ->get_record('assign_overrides', ['assignid' => $this->assign1->id, 'userid' => null, 'groupid' => $groupid]); - $this->assertEquals($override->groupid, $groupid); - - // Test group override set in the quiz. - $override = $DB->get_record('quiz_overrides', ['quiz' => $this->quiz1->id, 'userid' => null, 'groupid' => $groupid]); - $this->assertEquals($override->groupid, $groupid); - } - - /** - * Test the update SORA extension for students in a mapping with extension off. - * - * @covers \local_sitsgradepush\extensionmanager::update_sora_for_mapping - * @return void - * @throws \dml_exception|\coding_exception - */ - public function test_update_sora_for_mapping_with_extension_off(): void { - global $DB; - - // Set extension disabled. - set_config('extension_enabled', '0', 'local_sitsgradepush'); - - // The mapping inserted should be extension disabled. - $this->setup_for_sora_testing(); - - // Get mappings. - $mappings = manager::get_manager()->get_assessment_mappings_by_courseid($this->course1->id); - $mapping = reset($mappings); - // Process SORA extension for each mapping. - extensionmanager::update_sora_for_mapping($mapping, []); - - // Check error log. - $errormessage = get_string('error:extension_not_enabled_for_mapping', 'local_sitsgradepush', $mapping->id); - $sql = "SELECT * FROM {local_sitsgradepush_err_log} WHERE message = :message AND data = :data"; - $params = ['message' => $errormessage, 'data' => "Mapping ID: $mapping->id"]; - $log = $DB->get_record_sql($sql, $params); - $this->assertNotEmpty($log); - } - - /** - * Test the update SORA extension for students in a mapping. - * - * @covers \local_sitsgradepush\extensionmanager::update_sora_for_mapping - * @return void - * @throws \dml_exception|\coding_exception|\ReflectionException|\moodle_exception - */ - public function test_update_sora_for_mapping(): void { - global $DB; - - // Set up the SORA extension. - $this->setup_for_sora_testing(); - $manager = manager::get_manager(); - $apiclient = $this->get_apiclient_for_testing( - false, - [['code' => 12345678, 'assessment' => ['sora_assessment_duration' => 20, 'sora_rest_duration' => 5]]] - ); - tests_data_provider::set_protected_property($manager, 'apiclient', $apiclient); - - // Process all mappings for SORA. - $mappings = $manager->get_assessment_mappings_by_courseid($this->course1->id); - foreach ($mappings as $mapping) { - $students = $manager->get_students_from_sits($mapping); - extensionmanager::update_sora_for_mapping($mapping, $students); - } - - // Test SORA override group exists. - $groupid = $DB->get_field('groups', 'id', ['name' => sora::SORA_GROUP_PREFIX . '25']); - $this->assertNotEmpty($groupid); - - // Test user is added to the SORA group. - $groupmember = $DB->get_record('groups_members', ['groupid' => $groupid, 'userid' => $this->student1->id]); - $this->assertNotEmpty($groupmember); - - // Test group override set in the assignment. - $override = $DB - ->get_record('assign_overrides', ['assignid' => $this->assign1->id, 'userid' => null, 'groupid' => $groupid]); - $this->assertEquals($override->groupid, $groupid); - - // Test group override set in the quiz. - $override = $DB->get_record('quiz_overrides', ['quiz' => $this->quiz1->id, 'userid' => null, 'groupid' => $groupid]); - $this->assertEquals($override->groupid, $groupid); - } - /** * Test the user is enrolling a gradable role. * @@ -372,7 +179,7 @@ public function test_get_user_enrolment_events(): void { } $DB->insert_records('local_sitsgradepush_enrol', $events); - // Get user enrolment events. + // Get user enrolment events. Only 2 is returned as the max attempts is set to 2. $result = extensionmanager::get_user_enrolment_events(1); $this->assertCount(2, $result); } @@ -423,55 +230,4 @@ protected function setup_for_ec_testing(string $mapcode, string $mabseq, \stdCla return json_encode($message); } - - /** - * Set up the environment for SORA testing. - * @return void - * @throws \dml_exception - */ - protected function setup_for_sora_testing(): void { - global $DB; - $mabid = $DB->get_field('local_sitsgradepush_mab', 'id', ['mapcode' => 'LAWS0024A6UF', 'mabseq' => '001']); - $this->insert_mapping($mabid, $this->course1->id, $this->assign1, 'assign'); - - $mabid = $DB->get_field('local_sitsgradepush_mab', 'id', ['mapcode' => 'LAWS0024A6UF', 'mabseq' => '002']); - $this->insert_mapping($mabid, $this->course1->id, $this->quiz1, 'quiz'); - } - - /** - * Set up the SITS grade push. - * - * @return void - * @throws \dml_exception|\coding_exception - */ - private function setup_sitsgradepush(): void { - // Insert MABs. - tests_data_provider::import_sitsgradepush_grade_components(); - } - - /** - * Insert a test mapping. - * - * @param int $mabid - * @param int $courseid - * @param \stdClass $assessment - * @param string $modtype - * @return bool|int - * @throws \dml_exception - */ - private function insert_mapping(int $mabid, int $courseid, \stdClass $assessment, string $modtype): bool|int { - global $DB; - - return $DB->insert_record('local_sitsgradepush_mapping', [ - 'courseid' => $courseid, - 'sourceid' => $assessment->cmid, - 'sourcetype' => 'mod', - 'moduletype' => $modtype, - 'componentgradeid' => $mabid, - 'reassessment' => 0, - 'enableextension' => extensionmanager::is_extension_enabled() ? 1 : 0, - 'timecreated' => time(), - 'timemodified' => time(), - ]); - } } diff --git a/tests/extension/sora_test.php b/tests/extension/sora_test.php new file mode 100644 index 0000000..8ea1fe7 --- /dev/null +++ b/tests/extension/sora_test.php @@ -0,0 +1,266 @@ +. + +namespace local_sitsgradepush; + +use local_sitsgradepush\extension\sora; + +defined('MOODLE_INTERNAL') || die(); +global $CFG; +require_once($CFG->dirroot . '/local/sitsgradepush/tests/fixtures/tests_data_provider.php'); +require_once($CFG->dirroot . '/local/sitsgradepush/tests/extension/extension_common.php'); + +/** + * Tests for the SORA override. + * + * @package local_sitsgradepush + * @copyright 2024 onwards University College London {@link https://www.ucl.ac.uk/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Alex Yeung + */ +final class sora_test extends extension_common { + + /** + * Test no SORA override for non-exam assessments. + * + * @covers \local_sitsgradepush\assessment\assign::is_exam + * @covers \local_sitsgradepush\assessment\quiz::is_exam + * @return void + * @throws \coding_exception + * @throws \dml_exception + * @throws \moodle_exception + */ + public function test_no_sora_override_for_non_exam_assessment(): void { + global $DB; + + // Set up the SORA overrides. + $this->setup_for_sora_testing(); + + $startdate = strtotime('+1 hour'); + $enddate = strtotime('+7 days'); + + // Modify assignment so that open duration more than 5 hours, i.e. not exam. + $DB->update_record('assign', (object) [ + 'id' => $this->assign1->id, + 'allowsubmissionsfromdate' => $startdate, + 'duedate' => $enddate, + ]); + + // Modify quiz so that time limit more than 5 hours, i.e. not exam. + $DB->update_record('quiz', (object) [ + 'id' => $this->quiz1->id, + 'timelimit' => 2880, // 48 hours. + ]); + + // Get mappings. + $mappings = manager::get_manager()->get_assessment_mappings_by_courseid($this->course1->id); + $sora = new sora(); + $sora->set_properties_from_get_students_api(tests_data_provider::get_sora_testing_student_data()); + $sora->process_extension($mappings); + + // Test no SORA override for the assignment. + $override = $DB->record_exists('assign_overrides', ['assignid' => $this->assign1->id]); + $this->assertFalse($override); + + // Test no SORA override for the quiz. + $override = $DB->record_exists('quiz_overrides', ['quiz' => $this->quiz1->id]); + $this->assertFalse($override); + } + + /** + * Test no SORA override for past assessments. + * + * @covers \local_sitsgradepush\extension\sora::set_properties_from_get_students_api + * @return void + * @throws \coding_exception + * @throws \dml_exception + * @throws \moodle_exception + */ + public function test_no_sora_override_for_past_assessment(): void { + global $DB; + + // Set up the SORA overrides. + $this->setup_for_sora_testing(); + + // Modify assignment so that open duration more than 5 hours, i.e. not exam. + $DB->update_record('assign', (object) [ + 'id' => $this->assign1->id, + 'duedate' => strtotime('-1 day'), + ]); + + // Modify quiz so that time limit more than 5 hours, i.e. not exam. + $DB->update_record('quiz', (object) [ + 'id' => $this->quiz1->id, + 'timeclose' => strtotime('-1 day'), + ]); + + // Get mappings. + $mappings = manager::get_manager()->get_assessment_mappings_by_courseid($this->course1->id); + $sora = new sora(); + $sora->set_properties_from_get_students_api(tests_data_provider::get_sora_testing_student_data()); + $sora->process_extension($mappings); + + // Test no SORA override for the assignment. + $override = $DB->record_exists('assign_overrides', ['assignid' => $this->assign1->id]); + $this->assertFalse($override); + + // Test no SORA override for the quiz. + $override = $DB->record_exists('quiz_overrides', ['quiz' => $this->quiz1->id]); + $this->assertFalse($override); + } + + /** + * Test SORA override using data from AWS message. + * + * @covers \local_sitsgradepush\extension\extension::get_mappings_by_userid + * @covers \local_sitsgradepush\extension\sora::process_extension + * @covers \local_sitsgradepush\extension\sora::get_sora_group_id + * @covers \local_sitsgradepush\extension\sora::set_properties_from_aws_message + * @covers \local_sitsgradepush\assessment\assign::apply_sora_extension + * @covers \local_sitsgradepush\assessment\quiz::apply_sora_extension + * @throws \coding_exception + * @throws \dml_exception + * @throws \moodle_exception + */ + public function test_sora_process_extension_from_aws(): void { + global $DB; + + // Set up the SORA overrides. + $this->setup_for_sora_testing(); + + // Process the extension by passing the JSON event data. + $sora = new sora(); + $sora->set_properties_from_aws_message(tests_data_provider::get_sora_event_data()); + $sora->process_extension($sora->get_mappings_by_userid($sora->get_userid())); + + // Test SORA override group exists. + $groupid = $DB->get_field('groups', 'id', ['name' => $sora->get_extension_group_name()]); + $this->assertNotEmpty($groupid); + + // Test user is added to the SORA group. + $groupmember = $DB->get_record('groups_members', ['groupid' => $groupid, 'userid' => $this->student1->id]); + $this->assertNotEmpty($groupmember); + + // Test group override set in the assignment. + $override = $DB + ->get_record('assign_overrides', ['assignid' => $this->assign1->id, 'userid' => null, 'groupid' => $groupid]); + $this->assertEquals($override->groupid, $groupid); + + // Test group override set in the quiz. + $override = $DB->get_record('quiz_overrides', ['quiz' => $this->quiz1->id, 'userid' => null, 'groupid' => $groupid]); + $this->assertEquals($override->groupid, $groupid); + } + + /** + * Test the update SORA extension for students in a mapping with extension off. + * + * @covers \local_sitsgradepush\extensionmanager::update_sora_for_mapping + * @return void + * @throws \dml_exception|\coding_exception + */ + public function test_update_sora_for_mapping_with_extension_off(): void { + global $DB; + + // Set extension disabled. + set_config('extension_enabled', '0', 'local_sitsgradepush'); + + // The mapping inserted should be extension disabled. + $this->setup_for_sora_testing(); + + // Get mappings. + $mappings = manager::get_manager()->get_assessment_mappings_by_courseid($this->course1->id); + $mapping = reset($mappings); + // Process SORA extension for each mapping. + extensionmanager::update_sora_for_mapping($mapping, []); + + // Check error log. + $errormessage = get_string('error:extension_not_enabled_for_mapping', 'local_sitsgradepush', $mapping->id); + $sql = "SELECT * FROM {local_sitsgradepush_err_log} WHERE message = :message AND data = :data"; + $params = ['message' => $errormessage, 'data' => "Mapping ID: $mapping->id"]; + $log = $DB->get_record_sql($sql, $params); + $this->assertNotEmpty($log); + } + + /** + * Test the update SORA override for students in a mapping. + * It also tests the SORA override using the student data from assessment API + * and the SORA override group is deleted when the mapping is removed. + * + * @covers \local_sitsgradepush\extensionmanager::update_sora_for_mapping + * @covers \local_sitsgradepush\extensionmanager::delete_sora_overrides + * @covers \local_sitsgradepush\extensionmanager::get_default_sora_groups_ids_in_course + * @covers \local_sitsgradepush\manager::get_assessment_mappings_by_courseid + * @return void + * @throws \dml_exception|\coding_exception|\ReflectionException|\moodle_exception + */ + public function test_update_sora_for_mapping(): void { + global $DB; + + // Set up the SORA extension. + $this->setup_for_sora_testing(); + $manager = manager::get_manager(); + + // Process all mappings for SORA. + $mappings = $manager->get_assessment_mappings_by_courseid($this->course1->id); + foreach ($mappings as $mapping) { + extensionmanager::update_sora_for_mapping($mapping, [tests_data_provider::get_sora_testing_student_data()]); + } + + // Test SORA override group exists. + $groupid = $DB->get_field('groups', 'id', ['name' => sora::SORA_GROUP_PREFIX . '25']); + $this->assertNotEmpty($groupid); + + // Test user is added to the SORA group. + $groupmember = $DB->get_record('groups_members', ['groupid' => $groupid, 'userid' => $this->student1->id]); + $this->assertNotEmpty($groupmember); + + // Test group override set in the assignment. + $override = $DB + ->get_record('assign_overrides', ['assignid' => $this->assign1->id, 'userid' => null, 'groupid' => $groupid]); + $this->assertEquals($override->groupid, $groupid); + + // Test group override set in the quiz. + $override = $DB->get_record('quiz_overrides', ['quiz' => $this->quiz1->id, 'userid' => null, 'groupid' => $groupid]); + $this->assertEquals($override->groupid, $groupid); + + // Delete all mappings. + foreach ($mappings as $mapping) { + manager::get_manager()->remove_mapping($this->course1->id, $mapping->id); + } + + // Test SORA override group is deleted in the assignment. + $override = $DB->record_exists('assign_overrides', ['assignid' => $this->assign1->id]); + $this->assertFalse($override); + + // Test SORA override group is deleted in the quiz. + $override = $DB->record_exists('quiz_overrides', ['quiz' => $this->quiz1->id]); + $this->assertFalse($override); + } + + /** + * Set up the environment for SORA testing. + * @return void + * @throws \dml_exception + */ + protected function setup_for_sora_testing(): void { + global $DB; + $mabid = $DB->get_field('local_sitsgradepush_mab', 'id', ['mapcode' => 'LAWS0024A6UF', 'mabseq' => '001']); + $this->insert_mapping($mabid, $this->course1->id, $this->assign1, 'assign'); + + $mabid = $DB->get_field('local_sitsgradepush_mab', 'id', ['mapcode' => 'LAWS0024A6UF', 'mabseq' => '002']); + $this->insert_mapping($mabid, $this->course1->id, $this->quiz1, 'quiz'); + } +} diff --git a/tests/fixtures/sora_test_students.json b/tests/fixtures/sora_test_students.json new file mode 100644 index 0000000..6e4f894 --- /dev/null +++ b/tests/fixtures/sora_test_students.json @@ -0,0 +1,9 @@ +{ + "code": "12345678", + "spr_code": "12345678/1", + "assessment": { + "resit_number": 0, + "sora_assessment_duration": 20, + "sora_rest_duration": 5 + } +} diff --git a/tests/fixtures/tests_data_provider.php b/tests/fixtures/tests_data_provider.php index 28ca9f8..eb59932 100644 --- a/tests/fixtures/tests_data_provider.php +++ b/tests/fixtures/tests_data_provider.php @@ -188,6 +188,15 @@ public static function get_sora_event_data(): string { return file_get_contents(__DIR__ . "/sora_event_data.json"); } + /** + * Get the SORA testing student data. + * + * @return array + */ + public static function get_sora_testing_student_data(): array { + return json_decode(file_get_contents(__DIR__ . "/sora_test_students.json"), true); + } + /** * Set a protected property. *