From b3a2deaff253c9a018d74483e3573c2de8c925ea Mon Sep 17 00:00:00 2001 From: Alex Yeung Date: Thu, 21 Nov 2024 10:25:58 +0000 Subject: [PATCH] CTP-4066 SORA extenson for new student enrolment in course --- classes/cachemanager.php | 13 +++-- classes/extension/extension.php | 2 +- classes/extension/iextension.php | 2 +- classes/extension/sora.php | 14 +++-- classes/extension/sora_queue_processor.php | 6 ++- classes/extensionmanager.php | 59 ++++++++++++++++++---- classes/manager.php | 40 +++++++++++++-- classes/output/pushrecord.php | 2 +- classes/user_enrolment_callbacks.php | 59 ++++++++++++++++++++++ db/caches.php | 5 ++ db/hooks.php | 33 ++++++++++++ lang/en/local_sitsgradepush.php | 2 + tests/manager_test.php | 6 +-- version.php | 2 +- 14 files changed, 215 insertions(+), 30 deletions(-) create mode 100644 classes/user_enrolment_callbacks.php create mode 100644 db/hooks.php diff --git a/classes/cachemanager.php b/classes/cachemanager.php index 41334ed..91008d0 100644 --- a/classes/cachemanager.php +++ b/classes/cachemanager.php @@ -39,6 +39,9 @@ class cachemanager { /** @var string Cache area for storing marking schemes.*/ const CACHE_AREA_MARKINGSCHEMES = 'markingschemes'; + /** @var string Cache area for storing mapping and mab information.*/ + const CACHE_AREA_MAPPING_MAB_INFO = 'mappingmabinfo'; + /** * Get cache. * @@ -50,9 +53,13 @@ class cachemanager { public static function get_cache(string $area, string $key) { // Check if cache exists or expired. $cache = cache::make('local_sitsgradepush', $area)->get($key); - // Expire key. - $expires = 'expires_' . $key; + $expires = cache::make('local_sitsgradepush', $area)->get('expires_' . $key); + if (empty($cache) || empty($expires) || time() >= $expires) { + if (time() >= $expires) { + // Cache expired, delete it. + self::purge_cache($area, $key); + } return null; } else { return $cache; @@ -71,7 +78,7 @@ public static function get_cache(string $area, string $key) { public static function set_cache(string $area, string $key, mixed $value, int $expiresafter): void { $cache = cache::make('local_sitsgradepush', $area); $cache->set($key, $value); - $cache->set('expires_' . $key, $expiresafter); + $cache->set('expires_' . $key, time() + $expiresafter); } /** diff --git a/classes/extension/extension.php b/classes/extension/extension.php index 56179ff..b3b3799 100644 --- a/classes/extension/extension.php +++ b/classes/extension/extension.php @@ -125,7 +125,7 @@ protected function get_mappings_by_mab(string $mabidentifier): array { * @return array * @throws \dml_exception|\coding_exception */ - protected function get_mappings_by_userid(int $userid): array { + public function get_mappings_by_userid(int $userid): array { global $DB; // Find all enrolled courses for the student. diff --git a/classes/extension/iextension.php b/classes/extension/iextension.php index c494246..510a066 100644 --- a/classes/extension/iextension.php +++ b/classes/extension/iextension.php @@ -28,5 +28,5 @@ interface iextension { /** * Process the extension. */ - public function process_extension(): void; + public function process_extension(array $mappings): void; } diff --git a/classes/extension/sora.php b/classes/extension/sora.php index c44d89a..8ec2448 100644 --- a/classes/extension/sora.php +++ b/classes/extension/sora.php @@ -182,21 +182,25 @@ public function set_properties_from_get_students_api(array $student): void { /** * Process the extension. * + * @param array $mappings + * * @return void * @throws \coding_exception - * @throws \dml_exception + * @throws \dml_exception|\moodle_exception */ - public function process_extension(): void { + public function process_extension(array $mappings): void { if (!$this->dataisset) { throw new \coding_exception('error:extensiondataisnotset', 'local_sitsgradepush'); } - // Get all mappings for the student. - $mappings = $this->get_mappings_by_userid($this->get_userid()); + // Exit if SORA extra assessment duration and rest duration are both 0. + if ($this->extraduration == 0 && $this->restduration == 0) { + return; + } // No mappings found. if (empty($mappings)) { - return; + throw new \moodle_exception('error:mappings_is_empty', 'local_sitsgradepush'); } // Apply the extension to the assessments. diff --git a/classes/extension/sora_queue_processor.php b/classes/extension/sora_queue_processor.php index 3b84c6b..d725b31 100644 --- a/classes/extension/sora_queue_processor.php +++ b/classes/extension/sora_queue_processor.php @@ -16,6 +16,8 @@ namespace local_sitsgradepush\extension; +use local_sitsgradepush\manager; + /** * SORA queue processor. * @@ -47,6 +49,8 @@ protected function get_queue_url(): string { protected function process_message(array $messagebody): void { $sora = new sora(); $sora->set_properties_from_aws_message($messagebody['Message']); - $sora->process_extension(); + // Get all mappings for the student. + $mappings = $sora->get_mappings_by_userid($sora->get_userid()); + $sora->process_extension($mappings); } } diff --git a/classes/extensionmanager.php b/classes/extensionmanager.php index d1bb6ea..b2093de 100644 --- a/classes/extensionmanager.php +++ b/classes/extensionmanager.php @@ -31,23 +31,25 @@ class extensionmanager { /** * Update SORA extension for students in a mapping. * - * @param int $mapid + * @param int $mapid Assessment component mapping ID. + * @param int|null $userid Null to process all students, or user ID to process only that student. * @return void * @throws \dml_exception */ - public static function update_sora_for_mapping(int $mapid): void { + public static function update_sora_for_mapping(int $mapid, ?int $userid = null): void { try { - // Find the SITS assessment component. $manager = manager::get_manager(); - $mab = $manager->get_mab_by_mapping_id($mapid); - // Throw exception if the SITS assessment component is not found. - if (!$mab) { - throw new \moodle_exception('error:mab_not_found', 'local_sitsgradepush', '', $mapid); + // Find the SITS assessment component. + $mapinfo = $manager->get_mab_and_map_info_by_mapping_id($mapid); + + // Throw exception if the SITS assessment component or mapping is not found. + if (!$mapinfo) { + throw new \moodle_exception('error:mab_or_mapping_not_found', 'local_sitsgradepush', '', $mapid); } // Get students information for that assessment component. - $students = $manager->get_students_from_sits($mab); + $students = $manager->get_students_from_sits($mapinfo); // If no students found, nothing to do. if (empty($students)) { @@ -58,10 +60,49 @@ public static function update_sora_for_mapping(int $mapid): void { foreach ($students as $student) { $sora = new sora(); $sora->set_properties_from_get_students_api($student); - $sora->process_extension(); + // If user ID is set, only process the extension for that user. + if ($userid && $userid !== $sora->get_userid()) { + continue; + } + // Process the extension for all students. + $sora->process_extension([$mapinfo]); } } catch (\Exception $e) { logger::log($e->getMessage(), null, "Mapping ID: $mapid"); } } + + /** + * Check if the user has a gradable role in the course. + * + * @param int $userid User ID. + * @param int $courseid Course ID. + * @return bool + * @throws \coding_exception + * @throws \dml_exception + */ + public static function user_has_gradable_role(int $userid, int $courseid): bool { + global $CFG, $DB; + + $gradebookroles = explode(',', $CFG->gradebookroles); + if (empty($gradebookroles)) { + return false; + } + + [$insql, $inparam] = $DB->get_in_or_equal($gradebookroles, SQL_PARAMS_NAMED, 'roleid'); + + $sql = "SELECT COUNT(*) FROM {role_assignments} ra + JOIN {context} c ON ra.contextid = c.id + WHERE ra.userid = :userid + AND c.instanceid = :courseid + AND c.contextlevel = :contextlevel + AND ra.roleid $insql"; + $params = [ + 'userid' => $userid, + 'courseid' => $courseid, + 'contextlevel' => CONTEXT_COURSE, + ]; + + return $DB->count_records_sql($sql, array_merge($params, $inparam)) > 0; + } } diff --git a/classes/manager.php b/classes/manager.php index 5bc1662..55a7fb8 100644 --- a/classes/manager.php +++ b/classes/manager.php @@ -16,6 +16,8 @@ namespace local_sitsgradepush; +use cache; +use cache_store; use core_component; use core_course\customfield\course_handler; use DirectoryIterator; @@ -884,16 +886,44 @@ public function get_transfer_logs(int $assessmentmappingid, int $userid, ?string * @param int $id Assessment mapping ID. * * @return false|mixed - * @throws \dml_exception + * @throws \dml_exception|\coding_exception */ - public function get_mab_by_mapping_id(int $id): mixed { + public function get_mab_and_map_info_by_mapping_id(int $id): mixed { global $DB; - $sql = "SELECT cg.* + + // Try to get the cache first. + $key = 'map_mab_info_' . $id; + $cache = cachemanager::get_cache(cachemanager::CACHE_AREA_MAPPING_MAB_INFO, $key); + if (!empty($cache)) { + return $cache; + } + + // Define the SQL query for retrieving the information. + $sql = "SELECT + cg.*, + am.courseid, + am.sourceid, + am.sourcetype, + am.moduletype, + am.reassessment, + am.enableextension FROM {" . self::TABLE_COMPONENT_GRADE . "} cg - JOIN {" . self::TABLE_ASSESSMENT_MAPPING . "} am ON cg.id = am.componentgradeid + INNER JOIN {" . self::TABLE_ASSESSMENT_MAPPING . "} am + ON cg.id = am.componentgradeid WHERE am.id = :id"; - return $DB->get_record_sql($sql, ['id' => $id]); + // Fetch the record from the database. + $map_mab_info = $DB->get_record_sql($sql, ['id' => $id]); + if (!empty($map_mab_info)) { + // Set the cache. + cachemanager::set_cache( + cachemanager::CACHE_AREA_MAPPING_MAB_INFO, + $key, + $map_mab_info, + strtotime('+1 month') + ); + } + return $map_mab_info; } /** diff --git a/classes/output/pushrecord.php b/classes/output/pushrecord.php index 8a1281c..ececd50 100644 --- a/classes/output/pushrecord.php +++ b/classes/output/pushrecord.php @@ -262,7 +262,7 @@ protected function set_transfer_records(int $assessmentmappingid, int $studentid // The Easikit Get Student API will remove the students whose marks had been transferred successfully. // Find the assessment component - for that transfer log, // so that we can display the transfer status of mark transfer in the corresponding assessment component mapping. - $mab = $this->manager->get_mab_by_mapping_id($assessmentmappingid); + $mab = $this->manager->get_mab_and_map_info_by_mapping_id($assessmentmappingid); if (!empty($mab)) { $this->componentgrade = $mab->mapcode . '-' . $mab->mabseq; } diff --git a/classes/user_enrolment_callbacks.php b/classes/user_enrolment_callbacks.php new file mode 100644 index 0000000..3f4f249 --- /dev/null +++ b/classes/user_enrolment_callbacks.php @@ -0,0 +1,59 @@ +. + +namespace local_sitsgradepush; + +use core_enrol\hook\after_user_enrolled; + +/** + * Hook callbacks to get the enrolment information. + * + * @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 user_enrolment_callbacks { + + /** + * Callback for the user_enrolment hook. + * + * @param after_user_enrolled $hook + * @throws \dml_exception + */ + public static function process_extensions(after_user_enrolled $hook): void { + global $DB; + + $instance = $hook->get_enrolinstance(); + + // Check the enrolled user has a gradable role. + if (!extensionmanager::user_has_gradable_role($hook->get_userid(), $instance->courseid)) { + return; // User does not have a gradable role in the course, exit early. + } + + // Fetch all mappings for the course. + $mappings = $DB->get_records('local_sitsgradepush_mapping', ['courseid' => $instance->courseid]); + + if (empty($mappings)) { + return; // No mappings found, exit early. + } + + // Process each mapping in course. + foreach ($mappings as $mapping) { + extensionmanager::update_sora_for_mapping($mapping->id, $hook->get_userid()); + } + } +} diff --git a/db/caches.php b/db/caches.php index d89fe0b..b0d8c75 100644 --- a/db/caches.php +++ b/db/caches.php @@ -41,4 +41,9 @@ 'simplekeys' => true, 'simpledata' => false, ], + 'mappingmabinfo' => [ + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, + 'simpledata' => false, + ], ]; diff --git a/db/hooks.php b/db/hooks.php new file mode 100644 index 0000000..d22e6f6 --- /dev/null +++ b/db/hooks.php @@ -0,0 +1,33 @@ +. + +/** + * Hook callbacks for enrol_manual + * + * @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 + */ + +defined('MOODLE_INTERNAL') || die(); + +$callbacks = [ + [ + 'hook' => core_enrol\hook\after_user_enrolled::class, + 'callback' => 'local_sitsgradepush\user_enrolment_callbacks::process_extensions', + ], +]; diff --git a/lang/en/local_sitsgradepush.php b/lang/en/local_sitsgradepush.php index 8f64567..99cde82 100644 --- a/lang/en/local_sitsgradepush.php +++ b/lang/en/local_sitsgradepush.php @@ -113,7 +113,9 @@ $string['error:mab_has_push_records'] = 'Assessment component mapping cannot be updated as marks have been transfered for {$a}'; $string['error:mab_invalid_for_mapping'] = 'This assessment component is not valid for mapping due to the following reasons: {$a}.'; $string['error:mab_not_found'] = 'Assessment component not found. ID: {$a}'; +$string['error:mab_or_mapping_not_found'] = 'Mab or mapping not found. Mapping ID: {$a}'; $string['error:mapassessment'] = 'You do not have permission to map assessment.'; +$string['error:mappings_is_empty'] = 'Mappings is empty'; $string['error:marks_transfer_failed'] = 'Marks transfer failed.'; $string['error:missingparams'] = 'Missing parameters.'; $string['error:missingrequiredconfigs'] = 'Missing required configs.'; diff --git a/tests/manager_test.php b/tests/manager_test.php index 1418225..39cbc85 100644 --- a/tests/manager_test.php +++ b/tests/manager_test.php @@ -1269,19 +1269,19 @@ public function test_check_response(): void { /** * Test the get mab by mapping id method. * - * @covers \local_sitsgradepush\manager::get_mab_by_mapping_id + * @covers \local_sitsgradepush\manager::get_mab_and_map_info_by_mapping_id * @return void * @throws \ReflectionException * @throws \coding_exception * @throws \dml_exception * @throws \moodle_exception */ - public function test_get_mab_by_mapping_id(): void { + public function test_get_mab_and_map_info_by_mapping_id(): void { // Set up the test environment. $this->setup_testing_environment(assessmentfactory::get_assessment('mod', $this->assign1->cmid)); // Test the mab is returned. - $mab = $this->manager->get_mab_by_mapping_id($this->mappingid1); + $mab = $this->manager->get_mab_and_map_info_by_mapping_id($this->mappingid1); $this->assertEquals($this->mab1->id, $mab->id); } diff --git a/version.php b/version.php index 8068fdd..beb57dd 100644 --- a/version.php +++ b/version.php @@ -27,7 +27,7 @@ $plugin->component = 'local_sitsgradepush'; $plugin->release = '0.1.0'; -$plugin->version = 2024110100; +$plugin->version = 2024110101; $plugin->requires = 2024042200; $plugin->maturity = MATURITY_ALPHA; $plugin->dependencies = [