diff --git a/apiclients/easikit/classes/requests/getcomponentgrade.php b/apiclients/easikit/classes/requests/getcomponentgrade.php index 93874b7..c111c13 100644 --- a/apiclients/easikit/classes/requests/getcomponentgrade.php +++ b/apiclients/easikit/classes/requests/getcomponentgrade.php @@ -31,7 +31,7 @@ class getcomponentgrade extends request { 'mod_code' => 'MOD_CODE', 'mod_occ_year_code' => 'AYR_CODE', 'mod_occ_psl_code' => 'PSL_CODE', - 'mod_occ_mav' => 'MAV_OCCUR' + 'mod_occ_mav' => 'MAV_OCCUR', ]; /** @var string request method */ @@ -85,7 +85,7 @@ public function process_response($response): array { 'MAB_PERC' => $matches[1], 'MAB_NAME' => $value['name'], 'MKS_CODE' => $value['mark_scheme']['code'], - 'APA_ROMC' => $value['schedule']['location']['room']['identifier'] + 'APA_ROMC' => $value['schedule']['location']['room']['identifier'], ]; } } diff --git a/apiclients/easikit/classes/requests/getmarkingschemes.php b/apiclients/easikit/classes/requests/getmarkingschemes.php index 251cab4..2ab119e 100644 --- a/apiclients/easikit/classes/requests/getmarkingschemes.php +++ b/apiclients/easikit/classes/requests/getmarkingschemes.php @@ -25,6 +25,7 @@ * @author Alex Yeung */ class getmarkingschemes extends request { + /** @var string request method */ const METHOD = 'GET'; @@ -66,7 +67,7 @@ public function process_response($response): array { 'MKS_CODE' => $markingscheme['identifier'], 'MKS_MARKS' => $markingscheme['usage_indicator']['code'], 'MKS_TYPE' => $markingscheme['type']['code'], - 'MKS_IUSE' => $markingscheme['in_use_indicator'] + 'MKS_IUSE' => $markingscheme['in_use_indicator'], ]; } } diff --git a/apiclients/easikit/classes/requests/getstudent.php b/apiclients/easikit/classes/requests/getstudent.php index 5b7f3bc..08987eb 100644 --- a/apiclients/easikit/classes/requests/getstudent.php +++ b/apiclients/easikit/classes/requests/getstudent.php @@ -28,11 +28,12 @@ */ class getstudent extends request { + /** @var string[] Fields mapping - Local data fields to SITS' fields */ const FIELDS_MAPPING = [ 'idnumber' => 'STU_CODE', 'mapcode' => 'MAP_CODE', - 'mabseq' => 'MAB_SEQ' + 'mabseq' => 'MAB_SEQ', ]; /** @var string request method */ diff --git a/apiclients/easikit/classes/requests/getstudents.php b/apiclients/easikit/classes/requests/getstudents.php new file mode 100644 index 0000000..b8054a4 --- /dev/null +++ b/apiclients/easikit/classes/requests/getstudents.php @@ -0,0 +1,96 @@ +. + +namespace sitsapiclient_easikit\requests; + +use local_sitsgradepush\cachemanager; + +/** + * Class for getstudents request. + * + * @package sitsapiclient_easikit + * @copyright 2023 onwards University College London {@link https://www.ucl.ac.uk/} + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Alex Yeung + */ +class getstudents extends request { + + + + /** @var string[] Fields mapping - Local data fields to SITS' fields */ + const FIELDS_MAPPING = [ + 'mapcode' => 'MAP_CODE', + 'mabseq' => 'MAB_SEQ', + ]; + + /** @var string request method */ + const METHOD = 'GET'; + + /** + * Constructor. + * + * @param \stdClass $data + * @throws \dml_exception + * @throws \moodle_exception + */ + public function __construct(\stdClass $data) { + // Set request name. + $this->name = 'Get students'; + + // Get request endpoint. + $endpointurl = get_config('sitsapiclient_easikit', 'endpoint_get_student'); + + // Check if endpoint is set. + if (empty($endpointurl)) { + throw new \moodle_exception('Endpoint URL for ' . $this->name . ' is not set'); + } + + // Set the fields mapping, params fields and data. + parent::__construct(self::FIELDS_MAPPING, $endpointurl, $data); + } + + /** + * Process returned response. + * + * @param mixed $response + * @return array + */ + public function process_response($response): array { + $result = []; + if (!empty($response)) { + // Convert response to suitable format. + $response = json_decode($response, true); + $result = $response['response']['student_collection']['student'] ?? []; + } + + return $result; + } + + /** + * Get endpoint url with params. + * + * @return string + */ + public function get_endpoint_url_with_params(): string { + // Return endpoint url with params. + return sprintf( + '%s/%s-%s/student', + $this->endpointurl, + $this->paramsdata['MAP_CODE'], + $this->paramsdata['MAB_SEQ'] + ); + } +} diff --git a/apiclients/easikit/lib.php b/apiclients/easikit/lib.php index 0e284c1..ed75c22 100644 --- a/apiclients/easikit/lib.php +++ b/apiclients/easikit/lib.php @@ -23,6 +23,7 @@ use sitsapiclient_easikit\requests\getcomponentgrade; use sitsapiclient_easikit\requests\getmarkingschemes; use sitsapiclient_easikit\requests\getstudent; +use sitsapiclient_easikit\requests\getstudents; use sitsapiclient_easikit\requests\pushgrade; use sitsapiclient_easikit\requests\pushsubmissionlog; use sitsapiclient_easikit\requests\request; @@ -66,6 +67,9 @@ public function build_request(string $action, \stdClass $data = null, submission case manager::GET_STUDENT: $request = new getstudent($data); break; + case manager::GET_STUDENTS: + $request = new getstudents($data); + break; case manager::PUSH_SUBMISSION_LOG: $request = new pushsubmissionlog($data, $submission); break; diff --git a/apiclients/easikit/tests/privacy_provider_test.php b/apiclients/easikit/tests/privacy_provider_test.php index ff5d277..1c5f26a 100644 --- a/apiclients/easikit/tests/privacy_provider_test.php +++ b/apiclients/easikit/tests/privacy_provider_test.php @@ -27,6 +27,7 @@ * @author Alex Yeung */ class privacy_provider_test extends \advanced_testcase { + protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); @@ -39,7 +40,7 @@ protected function setUp(): void { * @return void * @throws \coding_exception */ - public function test_get_reason() { + public function test_get_reason(): void { $reason = get_string(provider::get_reason(), 'sitsapiclient_easikit'); $this->assertEquals('This plugin does not store any personal data.', $reason); } diff --git a/apiclients/easikit/version.php b/apiclients/easikit/version.php index 7c4e146..39cdc83 100644 --- a/apiclients/easikit/version.php +++ b/apiclients/easikit/version.php @@ -30,6 +30,6 @@ $plugin->version = 2023051900; $plugin->requires = 2021051708; $plugin->maturity = MATURITY_ALPHA; -$plugin->dependencies = array( - 'local_sitsgradepush' => 2022020101 -); +$plugin->dependencies = [ + 'local_sitsgradepush' => 2022020101, +]; diff --git a/apiclients/stutalkdirect/classes/requests/getcomponentgrade.php b/apiclients/stutalkdirect/classes/requests/getcomponentgrade.php index fb9dd16..9825148 100644 --- a/apiclients/stutalkdirect/classes/requests/getcomponentgrade.php +++ b/apiclients/stutalkdirect/classes/requests/getcomponentgrade.php @@ -31,7 +31,7 @@ class getcomponentgrade extends request { 'mod_code' => 'MOD_CODE', 'mod_occ_year_code' => 'AYR_CODE', 'mod_occ_psl_code' => 'PSL_CODE', - 'mod_occ_mav' => 'MAV_OCCUR' + 'mod_occ_mav' => 'MAV_OCCUR', ]; /** @var string[] Endpoint params */ @@ -60,7 +60,7 @@ public function __construct(\stdClass $data) { } // Set the fields mapping, params fields and data. - parent::__construct(self::FIELDS_MAPPING, $endpointurl, self::ENDPOINT_PARAMS, $data); + parent::__construct(self::FIELDS_MAPPING, $endpointurl, self::ENDPOINT_PARAMS, $data); } /** diff --git a/apiclients/stutalkdirect/classes/requests/getstudent.php b/apiclients/stutalkdirect/classes/requests/getstudent.php index 676ba81..c211939 100644 --- a/apiclients/stutalkdirect/classes/requests/getstudent.php +++ b/apiclients/stutalkdirect/classes/requests/getstudent.php @@ -30,7 +30,7 @@ class getstudent extends request { const FIELDS_MAPPING = [ 'idnumber' => 'STU_CODE', 'mapcode' => 'MAP_CODE', - 'mabseq' => 'MAB_SEQ' + 'mabseq' => 'MAB_SEQ', ]; /** @var string[] Endpoint params */ @@ -59,7 +59,7 @@ public function __construct(\stdClass $data) { } // Set the fields mapping, params fields and data. - parent::__construct(self::FIELDS_MAPPING, $endpointurl, self::ENDPOINT_PARAMS, $data); + parent::__construct(self::FIELDS_MAPPING, $endpointurl, self::ENDPOINT_PARAMS, $data); } /** diff --git a/apiclients/stutalkdirect/lib.php b/apiclients/stutalkdirect/lib.php index 0f564d0..c424dab 100644 --- a/apiclients/stutalkdirect/lib.php +++ b/apiclients/stutalkdirect/lib.php @@ -108,7 +108,7 @@ public function send_request(irequest $request) { curl_setopt( $curlclient, CURLOPT_HTTPHEADER, - array('Content-Type: application/json') + ['Content-Type: application/json'] ); } diff --git a/apiclients/stutalkdirect/tests/privacy_provider_test.php b/apiclients/stutalkdirect/tests/privacy_provider_test.php index ffd13d2..a6be89b 100644 --- a/apiclients/stutalkdirect/tests/privacy_provider_test.php +++ b/apiclients/stutalkdirect/tests/privacy_provider_test.php @@ -27,6 +27,7 @@ * @author Alex Yeung */ class privacy_provider_test extends \advanced_testcase { + protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); @@ -39,7 +40,7 @@ protected function setUp(): void { * @return void * @throws \coding_exception */ - public function test_get_reason() { + public function test_get_reason(): void { $reason = get_string(provider::get_reason(), 'sitsapiclient_stutalkdirect'); $this->assertEquals('This plugin does not store any personal data.', $reason); } diff --git a/apiclients/stutalkdirect/version.php b/apiclients/stutalkdirect/version.php index 03ee2ca..0f2a130 100644 --- a/apiclients/stutalkdirect/version.php +++ b/apiclients/stutalkdirect/version.php @@ -30,6 +30,6 @@ $plugin->version = 2023051700; $plugin->requires = 2021051708; $plugin->maturity = MATURITY_ALPHA; -$plugin->dependencies = array( - 'local_sitsgradepush' => 2022020101 -); +$plugin->dependencies = [ + 'local_sitsgradepush' => 2022020101, +]; diff --git a/classes/cachemanager.php b/classes/cachemanager.php new file mode 100644 index 0000000..5193cde --- /dev/null +++ b/classes/cachemanager.php @@ -0,0 +1,70 @@ +. + +namespace local_sitsgradepush; + +use cache; +use cache_application; +use cache_session; +use cache_store; + +/** + * Cache manager class for handling caches. + * + * @package local_sitsgradepush + * @copyright 2023 onwards University College London {@link https://www.ucl.ac.uk/} + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @author Alex Yeung + */ +class cachemanager { + /** @var string Cache area for storing students in an assessment component.*/ + const CACHE_AREA_STUDENTSPR = 'studentspr'; + + /** + * Get cache. + * + * @param string $area + * @param string $key + * @return cache_application|cache_session|cache_store|null + * @throws \coding_exception + */ + 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; + if (empty($cache) || empty($expires) || time() >= $expires) { + return null; + } else { + return $cache; + } + } + + /** + * Set cache. + * + * @param string $area + * @param string $key + * @param mixed $value + * @param int $expiresafter + * @return void + */ + public static function set_cache(string $area, string $key, mixed $value, int $expiresafter) { + $cache = cache::make('local_sitsgradepush', $area); + $cache->set($key, $value); + $cache->set('expires_' . $key, $expiresafter); + } +} diff --git a/classes/errormanager.php b/classes/errormanager.php index 0929959..b40a334 100644 --- a/classes/errormanager.php +++ b/classes/errormanager.php @@ -25,6 +25,7 @@ * @author Alex Yeung */ class errormanager { + /** @var int error type cannot be determined */ const ERROR_UNKNOWN = -99; @@ -66,7 +67,7 @@ class errormanager { self::ERROR_ATTEMPT_NUMBER_BLANK => 'Attempt number blank', self::ERROR_OVERWRITE_EXISTING_RECORD => 'Overwrite not allowed', self::ERROR_INVALID_MARKS => 'Invalid marks', - self::ERROR_INVALID_HAND_IN_STATUS => 'Invalid hand in status' + self::ERROR_INVALID_HAND_IN_STATUS => 'Invalid hand in status', ]; /** @var array error types and their match error strings */ @@ -82,7 +83,7 @@ class errormanager { 'no further update allowed', ], self::ERROR_INVALID_MARKS => ['Mark and/or Grade not valid'], - self::ERROR_INVALID_HAND_IN_STATUS => ['handin_status provided is not in SUS table'] + self::ERROR_INVALID_HAND_IN_STATUS => ['handin_status provided is not in SUS table'], ]; /** @@ -91,7 +92,7 @@ class errormanager { * @param int|null $errorcode error code * @return string error label */ - public static function get_error_label(int $errorcode = null) : string { + public static function get_error_label(int $errorcode = null): string { // If no error code provided, return unknown error. if (!isset($errorcode)) { return self::ERROR_TYPES_LABEL[self::ERROR_UNKNOWN]; @@ -106,7 +107,7 @@ public static function get_error_label(int $errorcode = null) : string { * @param string|null $errorstring * @return int */ - public static function identify_error(string $errorstring = null) : int { + public static function identify_error(string $errorstring = null): int { // If no error string provided, return unknown error. if (!isset($errorstring)) { return self::ERROR_UNKNOWN; diff --git a/classes/manager.php b/classes/manager.php index d86a2ed..78de11b 100644 --- a/classes/manager.php +++ b/classes/manager.php @@ -15,13 +15,13 @@ // along with Moodle. If not, see . namespace local_sitsgradepush; -use cache; use core_course\customfield\course_handler; use DirectoryIterator; use local_sitsgradepush\api\client_factory; use local_sitsgradepush\api\iclient; use local_sitsgradepush\api\irequest; use local_sitsgradepush\assessment\assessment; +use local_sitsgradepush\output\pushrecord; use local_sitsgradepush\submission\submissionfactory; defined('MOODLE_INTERNAL') || die; @@ -44,6 +44,9 @@ class manager { /** @var string Action identifier for get student from SITS */ const GET_STUDENT = 'getstudent'; + /** @var string Action identifier for get students from SITS */ + const GET_STUDENTS = 'getstudents'; + /** @var string Action identifier for pushing grades to SITS */ const PUSH_GRADE = 'pushgrade'; @@ -76,7 +79,7 @@ class manager { 'astcode' => 'AST_CODE', 'mabperc' => 'MAB_PERC', 'mabname' => 'MAB_NAME', - 'examroomcode' => 'APA_ROMC' + 'examroomcode' => 'APA_ROMC', ]; /** @var string[] Allowed activity types */ @@ -101,10 +104,10 @@ class manager { private static $instance = null; /** @var iclient|null API client for performing api calls */ - private $apiclient = null; + private ?iclient $apiclient = null; /** @var array Store any api errors */ - private $apierrors = []; + private array $apierrors = []; /** * Constructor. @@ -222,38 +225,44 @@ public function filter_out_invalid_component_grades(array $componentgrades): arr * Get options for component grade dropdown list in activity's settings page. * * @param int $courseid + * @param mixed $coursemoduleid * @return array * @throws \dml_exception */ - public function get_component_grade_options(int $courseid): array { + public function get_component_grade_options(int $courseid, mixed $coursemoduleid): array { $options = []; + // Get module occurrences from portico enrolments block. $modocc = \block_portico_enrolments\manager::get_modocc_mappings($courseid); + // Get local component grades records. + $records = $this->get_local_component_grades($modocc); + // Fetch component grades from SITS. - if ($this->fetch_component_grades_from_sits($modocc)) { - // Get the updated records from local component grades table. - $records = $this->get_local_component_grades($modocc); - if (!empty($records)) { - foreach ($records as $record) { - $option = new \stdClass(); - $option->disabled = ''; - $option->text = sprintf( - '%s-%s-%s-%s-%s %s', - $record->modcode, - $record->academicyear, - $record->periodslotcode, - $record->modocc, - $record->mabseq, - $record->mabname - ); - $option->value = $record->id; - if (!empty($record->assessmentmappingid)) { - $option->disabled = 'disabled'; - } - $options[] = $option; - } + // TODO: Could do some caching here. + $this->fetch_component_grades_from_sits($modocc); + $records = $this->get_local_component_grades($modocc); + + // Loop through records and build options. + foreach ($records as $record) { + $option = new \stdClass(); + $option->selected = ''; + + // This component grade is mapped to this activity, so set selected. + if (!empty($record->coursemoduleid) && $record->coursemoduleid == $coursemoduleid) { + $option->selected = 'selected'; } + $option->text = sprintf( + '%s-%s-%s-%s-%s %s', + $record->modcode, + $record->academicyear, + $record->periodslotcode, + $record->modocc, + $record->mabseq, + $record->mabname + ); + $option->value = $record->id; + $options[] = $option; } return $options; @@ -271,16 +280,16 @@ public function get_local_component_grades(array $modocc): array { $componentgrades = []; foreach ($modocc as $occ) { - $sql = "SELECT cg.*, am.id AS 'assessmentmappingid' + $sql = "SELECT cg.*, am.coursemoduleid AS 'coursemoduleid' FROM {" . self::TABLE_COMPONENT_GRADE . "} cg LEFT JOIN {" . self::TABLE_ASSESSMENT_MAPPING . "} am ON cg.id = am.componentgradeid WHERE cg.modcode = :modcode AND cg.modocc = :modocc AND cg.academicyear = :academicyear AND cg.periodslotcode = :periodslotcode"; - $params = array( + $params = [ 'modcode' => $occ->mod_code, 'modocc' => $occ->mod_occ_mav, 'academicyear' => $occ->mod_occ_year_code, - 'periodslotcode' => $occ->mod_occ_psl_code); + 'periodslotcode' => $occ->mod_occ_psl_code, ]; // Get AST codes. if ($astcodes = self::get_moodle_ast_codes()) { @@ -362,25 +371,45 @@ public function save_component_grades(array $componentgrades) { } /** - * Save assessment mapping to database. - * + * Save assessment mappings to database. * @param \stdClass $data * @return void + * @throws \coding_exception * @throws \dml_exception + * @throws \moodle_exception */ - public function save_assessment_mapping(\stdClass $data) { + public function save_assessment_mappings(\stdClass $data): void { global $DB; + // Remove any empty values. + $componentgrades = array_filter($data->gradepushassessmentselect); + + // Validate component grades. + $result = $this->validate_component_grades($componentgrades, $data->coursemodule); + + // Something went wrong, throw exception. + if ($result->errormessages) { + throw new \moodle_exception(implode('
', $result->errormessages)); + } - if (!$this->is_activity_mapped($data->coursemodule)) { - $record = new \stdClass(); - $record->courseid = $data->course; - $record->coursemoduleid = $data->coursemodule; - $record->moduletype = $data->modulename; - $record->componentgradeid = $data->gradepushassessmentselect; - $record->timecreated = time(); - $record->timemodified = time(); + // Delete existing mappings. + if ($result->mappingtoremove) { + foreach ($result->mappingtoremove as $mapping) { + $DB->delete_records(self::TABLE_ASSESSMENT_MAPPING, ['id' => $mapping->id]); + } + } - $DB->insert_record(self::TABLE_ASSESSMENT_MAPPING, $record); + // Insert new mappings. + if ($result->componentgradestomap) { + foreach ($result->componentgradestomap as $componentgradeid) { + $record = new \stdClass(); + $record->courseid = $data->course; + $record->coursemoduleid = $data->coursemodule; + $record->moduletype = $data->modulename; + $record->componentgradeid = $componentgradeid; + $record->timecreated = time(); + $record->timemodified = time(); + $DB->insert_record(self::TABLE_ASSESSMENT_MAPPING, $record); + } } } @@ -388,18 +417,20 @@ public function save_assessment_mapping(\stdClass $data) { * Lookup assessment mapping. * * @param int $cmid course module id - * @return false|mixed|\stdClass + * @return array * @throws \dml_exception */ public function get_assessment_mapping(int $cmid) { global $DB; $sql = "SELECT am.id, am.courseid, am.coursemoduleid, am.moduletype, am.componentgradeid, am.reassessment, am.reassessmentseq, cg.modcode, cg.modocc, cg.academicyear, cg.periodslotcode, cg.mapcode, - cg.mabseq, cg.astcode, cg.mabname + cg.mabseq, cg.astcode, cg.mabname, + CONCAT(cg.modcode, '-', cg.academicyear, '-', cg.periodslotcode, '-', cg.modocc, '-', + cg.mabseq, ' ', cg.mabname) AS 'formattedname' FROM {" . self::TABLE_ASSESSMENT_MAPPING . "} am JOIN {" . self::TABLE_COMPONENT_GRADE . "} cg ON am.componentgradeid = cg.id WHERE am.coursemoduleid = :cmid"; - return $DB->get_record_sql($sql, ['cmid' => $cmid]); + return $DB->get_records_sql($sql, ['cmid' => $cmid]); } /** @@ -418,12 +449,12 @@ public function is_activity_mapped(int $cmid): bool { * Check if the component grade is mapped to an activity. * * @param int $id - * @return bool + * @return mixed * @throws \dml_exception */ - public function is_component_grade_mapped(int $id): bool { + public function is_component_grade_mapped(int $id) { global $DB; - return $DB->record_exists(self::TABLE_ASSESSMENT_MAPPING, ['componentgradeid' => $id]); + return $DB->get_record(self::TABLE_ASSESSMENT_MAPPING, ['componentgradeid' => $id]); } /** @@ -497,65 +528,92 @@ public function get_local_component_grade_by_id(int $id) { * @throws \dml_exception * @throws \moodle_exception */ - public function get_student_from_sits(\stdClass $componentgrade, int $userid) { + public function get_student_from_sits(\stdClass $componentgrade, int $userid): mixed { + $studentspr = null; + // Get user. $user = user_get_users_by_id([$userid]); - // Try to get student spr from cache. - $cache = cache::make('local_sitsgradepush', 'studentspr'); - $sprcodecachekey = 'studentspr_' . $componentgrade->mapcode . '_' . $user[$userid]->idnumber; - $expirescachekey = 'expires_' . $componentgrade->mapcode . '_' . $user[$userid]->idnumber; - $studentspr = $cache->get($sprcodecachekey); - $expires = $cache->get($expirescachekey); - - // If cache is empty or expired, get student from SITS. - if (empty($studentspr) || empty($expires) || time() >= $expires) { - // Build required data. - $data = new \stdClass(); - $data->idnumber = $user[$userid]->idnumber; - $data->mapcode = $componentgrade->mapcode; - $data->mabseq = $componentgrade->mabseq; - - // Build and send request. - $request = $this->apiclient->build_request('getstudent', $data); - $response = $this->apiclient->send_request($request); + // Get students for the component grade. + $students = $this->get_students_from_sits($componentgrade); + foreach ($students as $student) { + if ($student['code'] == $user[$userid]->idnumber) { + $studentspr = $student['spr_code']; + } + } - // Check response. - $this->check_response($response, $request); + return $studentspr; + } + + /** + * Get students for a grade component from SITS. + * + * @param \stdClass $componentgrade + * @return \cache_application|\cache_session|\cache_store|mixed + * @throws \coding_exception + * @throws \dml_exception + * @throws \moodle_exception + */ + public function get_students_from_sits(\stdClass $componentgrade): mixed { + // Stutalk Direct is not supported currently. + if ($this->apiclient->get_client_name() == 'Stutalk Direct') { + throw new \moodle_exception( + get_string('error:multiplemappingsnotsupported', 'local_sitsgradepush', $this->apiclient->get_client_name() + )); + } - // Save response to cache. - $cache->set($sprcodecachekey, $response['SPR_CODE']); + // Try to get cache first. + $key = implode('_', [cachemanager::CACHE_AREA_STUDENTSPR, $componentgrade->mapcode, $componentgrade->mabseq]); + $students = cachemanager::get_cache(cachemanager::CACHE_AREA_STUDENTSPR, $key); - // Save expires to cache, expires in 30 days. - $cache->set($expirescachekey, time() + 2592000); + // Cache found, return students. + if (!empty($students)) { + return $students; + } - return $response['SPR_CODE']; - } else { - return $studentspr; + // Build required data. + $data = new \stdClass(); + $data->mapcode = $componentgrade->mapcode; + $data->mabseq = $componentgrade->mabseq; + + // Build and send request. + $request = $this->apiclient->build_request('getstudents', $data); + $result = $this->apiclient->send_request($request); + + // Set cache if result is not empty. + if (!empty($result)) { + cachemanager::set_cache( + cachemanager::CACHE_AREA_STUDENTSPR, + $key, + $result, + strtotime('+30 days'), + ); } + + return $result; } /** * Push grade to SITS. * - * @param assessment $assessment + * @param \stdClass $assessmentmapping * @param int $userid * @return bool * @throws \dml_exception * @throws \moodle_exception */ - public function push_grade_to_sits(assessment $assessment, int $userid) { + public function push_grade_to_sits(\stdClass $assessmentmapping, int $userid): bool { try { // Check if last push was succeeded, exit if succeeded. - if ($this->last_push_succeeded(self::PUSH_GRADE, $assessment->get_course_module()->id, $userid)) { + if ($this->last_push_succeeded($assessmentmapping->id, $userid, self::PUSH_GRADE)) { return false; } // Get required data for pushing. - $data = $this->get_required_data_for_pushing($assessment, $userid); + $data = $this->get_required_data_for_pushing($assessmentmapping, $userid); // Get grade. - $grade = $this->get_student_grade($assessment->get_course_module(), $userid); + $grade = $this->get_student_grade($assessmentmapping->coursemoduleid, $userid); // Push if grade is found. if ($grade->grade) { @@ -569,12 +627,13 @@ public function push_grade_to_sits(assessment $assessment, int $userid) { $this->check_response($response, $request); // Save transfer log. - $this->save_transfer_log(self::PUSH_GRADE, $assessment->get_course_module()->id, $userid, $request, $response); + $this->save_transfer_log(self::PUSH_GRADE, $assessmentmapping->id, $userid, $request, $response); return true; } } catch (\moodle_exception $e) { - $this->mark_push_as_failed(self::PUSH_GRADE, $assessment->get_course_module()->id, $userid, $e); + $this->mark_push_as_failed(self::PUSH_GRADE, $assessmentmapping->id, $userid, $e); + throw $e; } return false; @@ -583,12 +642,12 @@ public function push_grade_to_sits(assessment $assessment, int $userid) { /** * Push submission log to SITS. * - * @param assessment $assessment + * @param \stdClass $assessmentmapping * @param int $userid * @return bool * @throws \dml_exception */ - public function push_submission_log_to_sits(assessment $assessment, int $userid): bool { + public function push_submission_log_to_sits(\stdClass $assessmentmapping, int $userid): bool { try { // Check if submission log push is enabled. if (!get_config('local_sitsgradepush', 'sublogpush')) { @@ -596,15 +655,15 @@ public function push_submission_log_to_sits(assessment $assessment, int $userid) } // Check if last push was succeeded, exit if succeeded. - if ($this->last_push_succeeded(self::PUSH_SUBMISSION_LOG, $assessment->get_course_module()->id, $userid)) { + if ($this->last_push_succeeded($assessmentmapping->id, $userid, self::PUSH_SUBMISSION_LOG)) { return false; } // Get required data for pushing. - $data = $this->get_required_data_for_pushing($assessment, $userid); + $data = $this->get_required_data_for_pushing($assessmentmapping, $userid); // Create the submission object. - $submission = submissionfactory::get_submission($assessment->get_course_module(), $userid); + $submission = submissionfactory::get_submission($assessmentmapping->coursemoduleid, $userid); // Push if student has submission. if ($submission->get_submission_data()) { @@ -616,12 +675,12 @@ public function push_submission_log_to_sits(assessment $assessment, int $userid) // Save push log. $this->save_transfer_log( - self::PUSH_SUBMISSION_LOG, $assessment->get_course_module()->id, $userid, $request, $response); + self::PUSH_SUBMISSION_LOG, $assessmentmapping->id, $userid, $request, $response); return true; } } catch (\moodle_exception $e) { - $this->mark_push_as_failed(self::PUSH_SUBMISSION_LOG, $assessment->get_course_module()->id, $userid, $e); + $this->mark_push_as_failed(self::PUSH_SUBMISSION_LOG, $assessmentmapping->id, $userid, $e); } return false; @@ -630,40 +689,31 @@ public function push_submission_log_to_sits(assessment $assessment, int $userid) /** * Get required data for pushing. * - * @param assessment $assessment + * @param \stdClass $assessmentmapping * @param int $userid * @return \stdClass * @throws \coding_exception * @throws \dml_exception * @throws \moodle_exception */ - public function get_required_data_for_pushing (assessment $assessment, int $userid): \stdClass { + public function get_required_data_for_pushing (\stdClass $assessmentmapping, int $userid): \stdClass { global $USER; - // Get assessment mapping. - $assessmentinfo = 'CMID: ' .$assessment->get_course_module()->id . ', USERID: ' . $userid; - - // Check mapping. - if (!$mapping = $this->get_assessment_mapping($assessment->get_course_module()->id)) { - logger::log(get_string('error:assessmentmapping', 'local_sitsgradepush', $assessmentinfo)); - throw new \moodle_exception('error:assessmentisnotmapped', 'local_sitsgradepush', '', $assessmentinfo); - } - // Get SPR_CODE from SITS. - $studentspr = $this->get_student_from_sits($mapping, $userid); + $studentspr = $this->get_student_from_sits($assessmentmapping, $userid); // Build the required data. $data = new \stdClass(); - $data->mapcode = $mapping->mapcode; - $data->mabseq = $mapping->mabseq; + $data->mapcode = $assessmentmapping->mapcode; + $data->mabseq = $assessmentmapping->mabseq; $data->sprcode = $studentspr; - $data->academicyear = $mapping->academicyear; - $data->pslcode = $mapping->periodslotcode; - $data->reassessment = $mapping->reassessment; + $data->academicyear = $assessmentmapping->academicyear; + $data->pslcode = $assessmentmapping->periodslotcode; + $data->reassessment = $assessmentmapping->reassessment; $data->source = sprintf( 'moodle-course%s-activity%s-user%s', - $assessment->get_course_module()->course, - $assessment->get_course_module()->id, + $assessmentmapping->courseid, + $assessmentmapping->coursemoduleid, $USER->id ); $data->srarseq = '001'; // Just a dummy reassessment sequence number for now. @@ -674,20 +724,42 @@ public function get_required_data_for_pushing (assessment $assessment, int $user /** * Return the last push log for a given grade push. * - * @param string $type - * @param int $coursemoduleid + * @param int $assessmentmappingid * @param int $userid - * @return false|mixed + * @param string|null $type + * @return array * @throws \dml_exception */ - public function get_transfer_log (string $type, int $coursemoduleid, int $userid) { + public function get_transfer_log (int $assessmentmappingid, int $userid, string $type = null): array { global $DB; - $sql = "SELECT trflog.id, trflog.response, trflog.timecreated, errlog.errortype, errlog.message - FROM {" . self::TABLE_TRANSFER_LOG . "} trflog LEFT JOIN - {" . self::TABLE_ERROR_LOG . "} errlog ON trflog.errlogid = errlog.id - WHERE type = :type AND trflog.userid = :userid AND coursemoduleid = :coursemoduleid - ORDER BY timecreated DESC LIMIT 1"; - return $DB->get_record_sql($sql, ['type' => $type, 'coursemoduleid' => $coursemoduleid, 'userid' => $userid]); + + // Initialize params. + $params = [ + 'assessmentmappingid' => $assessmentmappingid, + 'userid1' => $userid, + 'userid2' => $userid, + ]; + + // Filter by type if given. + $bytype = ''; + if (!empty($type)) { + $bytype = 'AND t1.type = :type'; + $params['type'] = $type; + } + + // Get the latest record for each push type, e.g. grade push, submission log push. + $sql = "SELECT t1.id, t1.type, t1.request, t1.response, t1.timecreated, t1.errlogid, errlog.errortype, errlog.message + FROM {" . self::TABLE_TRANSFER_LOG . "} t1 + INNER JOIN ( + SELECT type, MAX(timecreated) AS latest_time + FROM {" . self::TABLE_TRANSFER_LOG . "} + WHERE userid = :userid1 + GROUP BY type) t2 + ON t1.type = t2.type AND t1.timecreated = t2.latest_time + LEFT JOIN {" . self::TABLE_ERROR_LOG . "} errlog ON t1.errlogid = errlog.id + WHERE t1.userid = :userid2 AND t1.assessmentmappingid = :assessmentmappingid {$bytype}"; + + return $DB->get_records_sql($sql, $params); } /** @@ -699,61 +771,52 @@ public function get_transfer_log (string $type, int $coursemoduleid, int $userid * @throws \dml_exception * @throws \moodle_exception */ - public function get_assessment_data(assessment $assessment) { + public function get_assessment_data(assessment $assessment): array { $assessmentdata = []; + $assessmentdata['studentsnotrecognized'] = []; $students = $assessment->get_all_participants(); $coursemodule = $assessment->get_course_module(); - foreach ($students as $student) { - $grade = $this->get_student_grade($coursemodule, $student->id); - if (!empty($grade->grade)) { - $data = new \stdClass(); - $data->userid = $student->id; - $data->idnumber = $student->idnumber; - $data->firstname = $student->firstname; - $data->lastname = $student->lastname; - $data->marks = $grade->grade; - $data->handin_datetime = '-'; - $data->handin_status = '-'; - $data->export_staff = '-'; - $data->lastgradepushresult = null; - $data->lastgradepusherrortype = null; - $data->lastgradepushtime = '-'; - $data->lastsublogpushresult = null; - $data->lastsublogpusherrortype = null; - $data->lastsublogpushtime = '-'; - - // Get grade push status. - if ($gradepushstatus = $this->get_transfer_log(self::PUSH_GRADE, $coursemodule->id, $student->id)) { - $response = json_decode($gradepushstatus->response); - $errortype = $gradepushstatus->errortype ?: errormanager::ERROR_UNKNOWN; - $data->lastgradepushresult = ($response->code == '0') ? 'success' : 'failed'; - $data->lastgradepusherrortype = ($response->code == '0') ? 0 : $errortype; - $data->lastgradepushtime = date('Y-m-d H:i:s', $gradepushstatus->timecreated); - } - // Get submission log push status. - if ($gradepushstatus = $this->get_transfer_log(self::PUSH_SUBMISSION_LOG, $coursemodule->id, $student->id)) { - $response = json_decode($gradepushstatus->response); - $errortype = $gradepushstatus->errortype ?: errormanager::ERROR_UNKNOWN; - $data->lastsublogpushresult = ($response->code == '0') ? 'success' : 'failed'; - $data->lastsublogpusherrortype = ($response->code == '0') ? 0 : $errortype; - $data->lastsublogpushtime = date('Y-m-d H:i:s', $gradepushstatus->timecreated); - } + // Get assessment mappings. + $mappings = $this->get_assessment_mapping($coursemodule->id); + if (empty($mappings)) { + return []; + } - // Get submission. - $submission = submissionfactory::get_submission($coursemodule, $student->id); - if ($submission->get_submission_data()) { - $data->handin_datetime = $submission->get_handin_datetime(); - $data->handin_status = $submission->get_handin_status(); - $data->export_staff = $submission->get_export_staff(); + // Fetch students from SITS. + foreach ($mappings as $mapping) { + $mabkey = $mapping->mapcode . '-' . $mapping->mabseq; + $studentsfromsits[$mabkey] = + array_column($this->get_students_from_sits($mapping), 'code'); + $assessmentdata['mappings'][$mabkey] = $mapping; + + foreach ($students as $key => $student) { + $studentrecord = new pushrecord($student, $coursemodule->id, $mapping); + // Add students who have push records of this mapping to the mapping's students array. + if ($studentrecord->componentgrade == $mabkey || in_array($studentrecord->idnumber, $studentsfromsits[$mabkey])) { + $assessmentdata['mappings'][$mabkey]->students[] = $studentrecord; + unset($students[$key]); } + } + } + + // Remaining students are not valid for pushing. + $invalidstudents = new \stdClass(); + $invalidstudents->formattedname = get_string('invalidstudents', 'local_sitsgradepush'); + $invalidstudents->students = []; + foreach ($students as $student) { + $invalidstudents->students[] = new pushrecord($student, $coursemodule->id); + } + $assessmentdata['invalidstudents'] = $invalidstudents; - $assessmentdata[] = $data; + // Sort students for each mapping. + foreach ($assessmentdata['mappings'] as $mapping) { + if (!empty($mapping->students)) { + $mapping->students = $this->sort_grade_push_history_table($mapping->students); } } - // Sort data. - return $this->sort_grade_push_history_table($assessmentdata); + return $assessmentdata; } /** @@ -834,7 +897,7 @@ public function is_current_academic_year_activity(int $courseid): bool { * @return array|false * @throws \dml_exception */ - public function get_moodle_ast_codes() { + public function get_moodle_ast_codes(): bool|array { $codes = get_config('local_sitsgradepush', 'moodle_ast_codes'); if (!empty($codes)) { if ($codes = explode(',', $codes)) { @@ -851,7 +914,7 @@ public function get_moodle_ast_codes() { * @return array|false * @throws \dml_exception */ - public function get_moodle_ast_codes_work_with_exam_room_code() { + public function get_moodle_ast_codes_work_with_exam_room_code(): bool|array { $codes = get_config('local_sitsgradepush', 'moodle_ast_codes_exam_room'); if (!empty($codes)) { if ($codes = explode(',', $codes)) { @@ -869,7 +932,7 @@ public function get_moodle_ast_codes_work_with_exam_room_code() { * @return bool|int * @throws \dml_exception */ - public function schedule_push_task(int $coursemoduleid) { + public function schedule_push_task(int $coursemoduleid): bool|int { global $DB, $USER; // Check course module exists. @@ -877,7 +940,7 @@ public function schedule_push_task(int $coursemoduleid) { throw new \moodle_exception('error:coursemodulenotfound', 'local_sitsgradepush'); } - // Check if course module has been mapped to an assessment component. + // Check if the course module has been mapped to an assessment component. if (!$DB->record_exists('local_sitsgradepush_mapping', ['coursemoduleid' => $coursemoduleid])) { throw new \moodle_exception('error:assessmentisnotmapped', 'local_sitsgradepush'); } @@ -903,7 +966,7 @@ public function schedule_push_task(int $coursemoduleid) { * @throws \coding_exception * @throws \dml_exception */ - public function get_pending_task_in_queue(int $coursemoduleid) { + public function get_pending_task_in_queue(int $coursemoduleid): bool|\stdClass { global $DB; $sql = 'SELECT * FROM {local_sitsgradepush_tasks} @@ -942,7 +1005,7 @@ public function get_pending_task_in_queue(int $coursemoduleid) { * @return false|mixed * @throws \dml_exception */ - public function get_last_finished_push_task(int $coursemoduleid) { + public function get_last_finished_push_task(int $coursemoduleid): mixed { global $DB; // Get the last task for the course module. $sql = 'SELECT * @@ -979,7 +1042,7 @@ public function get_last_finished_push_task(int $coursemoduleid) { * @return int * @throws \dml_exception */ - public function get_number_of_running_tasks() { + public function get_number_of_running_tasks(): int { global $DB; return $DB->count_records('local_sitsgradepush_tasks', ['status' => self::PUSH_TASK_STATUS_PROCESSING]); } @@ -992,7 +1055,7 @@ public function get_number_of_running_tasks() { * @return array * @throws \dml_exception */ - public function get_push_tasks(int $status, int $limit) { + public function get_push_tasks(int $status, int $limit): array { global $DB; return $DB->get_records('local_sitsgradepush_tasks', ['status' => $status], 'timescheduled ASC', '*', 0, $limit); } @@ -1006,7 +1069,7 @@ public function get_push_tasks(int $status, int $limit) { * @return void * @throws \dml_exception */ - public function update_push_task_status(int $id, int $status, int $errlogid = null) { + public function update_push_task_status(int $id, int $status, int $errlogid = null): void { global $DB; $task = $DB->get_record('local_sitsgradepush_tasks', ['id' => $id]); $task->status = $status; @@ -1034,7 +1097,7 @@ public function get_user_profile_fields(): array { * @return mixed|\stdClass|string * @throws \dml_exception */ - public function get_export_staff() { + public function get_export_staff(): mixed { global $USER, $DB; // Get the source field from config. @@ -1050,14 +1113,140 @@ public function get_export_staff() { return ''; } + /** + * Return formatted component grade name. + * + * @param int $componentgradeid + * @return string + * @throws \dml_exception + */ + public function get_formatted_component_grade_name(int $componentgradeid): string { + $formattedname = ''; + + if ($componentgrade = $this->get_local_component_grade_by_id($componentgradeid)) { + $formattedname = sprintf( + '%s-%s-%s-%s-%s %s', + $componentgrade->modcode, + $componentgrade->academicyear, + $componentgrade->periodslotcode, + $componentgrade->modocc, + $componentgrade->mabseq, + $componentgrade->mabname + ); + } + + return $formattedname; + } + + /** + * Check if any grade had been pushed for an assessment mapping. + * @param int $assessmentmappingid + * @return bool + * @throws \dml_exception + */ + public function has_grades_pushed(int $assessmentmappingid): bool { + global $DB; + return $DB->record_exists(self::TABLE_TRANSFER_LOG, ['assessmentmappingid' => $assessmentmappingid]); + } + + /** + * Validate component grades submitted from the form. + * + * @param array $componentgrades + * @param int|null $coursemoduleid + * @return \stdClass + * @throws \coding_exception + * @throws \dml_exception + */ + public function validate_component_grades(array $componentgrades, int $coursemoduleid = null): \stdClass { + $errormessages = []; + $componentgradestomap = []; + $mappingtoremove = []; + $duplicatemappings = []; + + // Count the number of component grades have same map code. + foreach ($componentgrades as $componentgradeid) { + $componentgrade = $this->get_local_component_grade_by_id($componentgradeid); + $duplicatemappings[$componentgrade->mapcode][] = $componentgrade; + } + + // Check if more than one component grade with same map code is mapped to the same activity. + foreach ($duplicatemappings as $mapcode => $componentgradesarray) { + if (count($componentgradesarray) > 1) { + $errormessages[] = get_string('error:duplicatemapping', 'local_sitsgradepush', $mapcode); + } + } + + // For newly created activity, no need to check existing mapping. + if (!$coursemoduleid) { + $componentgradestomap = $componentgrades; + } else { + // Get existing component grades mapping for the course module. + $existingmapping = $this->get_assessment_mapping($coursemoduleid); + + if ($existingmapping) { + // Extract mapped component grades. + $mappedcomponentgrades = array_column($existingmapping, 'componentgradeid'); + + foreach ($componentgrades as $componentgradeid) { + // Mapped component grade does not need to be mapped again. + if (!in_array($componentgradeid, $mappedcomponentgrades)) { + $componentgradestomap[] = $componentgradeid; + } + } + + // Get the mappings that need to be removed. + foreach ($existingmapping as $mapping) { + if (!in_array($mapping->componentgradeid, $componentgrades)) { + $mappingtoremove[] = $mapping; + } + } + } else { + // No existing mapping, try to map all component grades. + $componentgradestomap = $componentgrades; + } + } + + // Check if any component grade had been mapped to another activity. + foreach ($componentgradestomap as $componentgradeid) { + // Check if any component grade had been mapped to another activity. + if ($this->is_component_grade_mapped($componentgradeid)) { + $componentgradename = $this->get_formatted_component_grade_name($componentgradeid); + $errormessages[] = get_string('error:componentgrademapped', 'local_sitsgradepush', $componentgradename); + } + } + + // Removing mapping is not allowed if there is any grade had been pushed. + foreach ($mappingtoremove as $mapping) { + if ($this->has_grades_pushed($mapping->id)) { + $componentgradename = $this->get_formatted_component_grade_name($mapping->componentgradeid); + $errormessages[] = get_string('error:componentgradepushed', 'local_sitsgradepush', $componentgradename); + } + } + + // Return result. + $result = new \stdClass(); + $result->componentgradestomap = $componentgradestomap; + $result->mappingtoremove = $mappingtoremove; + $result->errormessages = $errormessages; + + return $result; + } + /** * Get grade of an assessment for a student. * - * @param \stdClass $coursemodule + * @param int $coursemoduleid * @param int $userid * @return \stdClass|null + * @throws \coding_exception + * @throws \moodle_exception */ - private function get_student_grade(\stdClass $coursemodule, int $userid): ?\stdClass { + public function get_student_grade(int $coursemoduleid, int $userid): ?\stdClass { + $coursemodule = get_coursemodule_from_id('', $coursemoduleid); + if (empty($coursemodule)) { + throw new \moodle_exception('error:coursemodulenotfound', 'local_sitsgradepush', '', $coursemoduleid); + } // Return grade of the first grade item. if ($grade = grade_get_grades($coursemodule->course, 'mod', $coursemodule->modname, $coursemodule->instance, $userid)) { foreach ($grade->items as $item) { @@ -1074,7 +1263,7 @@ private function get_student_grade(\stdClass $coursemodule, int $userid): ?\stdC * Save transfer log. * * @param string $type - * @param int $coursemoduleid + * @param int $assessmentmappingid * @param int $userid * @param mixed $request * @param array $response @@ -1083,12 +1272,12 @@ private function get_student_grade(\stdClass $coursemodule, int $userid): ?\stdC * @throws \dml_exception */ private function save_transfer_log( - string $type, int $coursemoduleid, int $userid, $request, array $response, int $errorlogid = null) { + string $type, int $assessmentmappingid, int $userid, mixed $request, array $response, int $errorlogid = null): void { global $USER, $DB; $insert = new \stdClass(); $insert->type = $type; $insert->userid = $userid; - $insert->coursemoduleid = $coursemoduleid; + $insert->assessmentmappingid = $assessmentmappingid; $insert->request = ($request instanceof irequest) ? $request->get_endpoint_url_with_params() : null; $insert->requestbody = ($request instanceof irequest) ? $request->get_request_body() : null; $insert->response = json_encode($response); @@ -1102,14 +1291,16 @@ private function save_transfer_log( /** * Check if the last push was succeeded. * - * @param string $pushtype - * @param int $coursemoduleid + * @param int $assessmentmappingid * @param int $userid + * @param string $pushtype * @return bool * @throws \dml_exception */ - private function last_push_succeeded(string $pushtype, int $coursemoduleid, int $userid): bool { - if ($log = $this->get_transfer_log($pushtype, $coursemoduleid, $userid)) { + private function last_push_succeeded(int $assessmentmappingid, int $userid, string $pushtype): bool { + if ($log = $this->get_transfer_log($assessmentmappingid, $userid, $pushtype)) { + // Get the first element. + $log = reset($log); if (!empty($log->response)) { $response = json_decode($log->response); // Last push was succeeded. No need to push again. @@ -1132,7 +1323,7 @@ private function last_push_succeeded(string $pushtype, int $coursemoduleid, int * @throws \dml_exception * @throws \moodle_exception */ - private function check_response($response, irequest $request) { + private function check_response(mixed $response, irequest $request): void { // Throw exception when response is empty. if (empty($response)) { // If request is get student, return a student not found message for the logger to identify the error. @@ -1163,24 +1354,24 @@ private function check_response($response, irequest $request) { * Add failed transfer log. * * @param string $requestidentifier - * @param int $coursemoduleid + * @param int $assessmentmappingid * @param int $userid * @param \moodle_exception $exception * @return void * @throws \dml_exception */ private function mark_push_as_failed( - string $requestidentifier, int $coursemoduleid, int $userid, \moodle_exception $exception) { + string $requestidentifier, int $assessmentmappingid, int $userid, \moodle_exception $exception): void { // Failed response. $response = [ "code" => "-1", - "message" => $exception->getMessage() + "message" => $exception->getMessage(), ]; // Get error log id if any. $errorlogid = $exception->debuginfo ?: null; // Add failed transfer log. - $this->save_transfer_log($requestidentifier, $coursemoduleid, $userid, null, $response, $errorlogid); + $this->save_transfer_log($requestidentifier, $assessmentmappingid, $userid, null, $response, intval($errorlogid)); } } diff --git a/classes/output/pushrecord.php b/classes/output/pushrecord.php new file mode 100644 index 0000000..b4ba240 --- /dev/null +++ b/classes/output/pushrecord.php @@ -0,0 +1,190 @@ +. + +namespace local_sitsgradepush\output; + +use local_sitsgradepush\errormanager; +use local_sitsgradepush\manager; +use local_sitsgradepush\submission\submissionfactory; + +/** + * Push record object for display in the grade push page. + * + * @package local_sitsgradepush + * @copyright 2023 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 pushrecord { + /** @var string SITS component grade */ + public string $componentgrade = ''; + + /** @var string User id */ + public string $userid; + + /** @var string Student idnumber */ + public string $idnumber; + + /** @var string Student firstname */ + public string $firstname; + + /** @var string Student lastname */ + public string $lastname; + + /** @var string Student marks */ + public string $marks = '-'; + + /** @var string Student hand in date time */ + public string $handindatetime = '-'; + + /** @var string Submission hand in status */ + public string $handinstatus = '-'; + + /** @var string Submission export staff */ + public string $exportstaff = '-'; + + /** @var string|null Last grade push result */ + public ?string $lastgradepushresult = null; + + /** @var int Last grade push error type */ + public int $lastgradepusherrortype = 0; + + /** @var string Last grade push time */ + public string $lastgradepushtime = '-'; + + /** @var string|null Last submission log push result */ + public ?string $lastsublogpushresult = null; + + /** @var int Last submission log push error type */ + public int $lastsublogpusherrortype = 0; + + /** @var string Last submission log push time */ + public string $lastsublogpushtime = '-'; + + /** @var manager|null Grade push manager */ + protected ?manager $manager; + + /** + * Constructor. + * + * @param \stdClass $student + * @param int $coursemoduleid + * @param \stdClass|null $mapping + * @throws \dml_exception + * @throws \moodle_exception + */ + public function __construct(\stdClass $student, int $coursemoduleid, \stdClass $mapping = null) { + // Get manager. + $this->manager = manager::get_manager(); + + // Set student data. + $this->set_student_info($student); + + // Set grade. + $this->set_grade($coursemoduleid, $student->id); + + // Set submission. + $this->set_submission($coursemoduleid, $student->id); + + if (!empty($mapping)) { + // Set transfer records. + $this->set_transfer_records($mapping->id, $student->id); + } + } + + /** + * Set grade. + * + * @param int $coursemoduleid + * @param int $studentid + * @return void + * @throws \moodle_exception + */ + protected function set_grade (int $coursemoduleid, int $studentid): void { + $grade = $this->manager->get_student_grade($coursemoduleid, $studentid); + if (!empty($grade->grade)) { + $this->marks = $grade->grade; + } + } + + /** + * Set student info. + * @param \stdClass $student + * @return void + */ + protected function set_student_info (\stdClass $student): void { + $this->userid = $student->id; + $this->idnumber = $student->idnumber; + $this->firstname = $student->firstname; + $this->lastname = $student->lastname; + } + + /** + * Set submission. + * @param int $coursemoduleid + * @param int $studentid + * @return void + * @throws \coding_exception + * @throws \dml_exception + * @throws \moodle_exception + */ + protected function set_submission (int $coursemoduleid, int $studentid): void { + + // Get submission. + $submission = submissionfactory::get_submission($coursemoduleid, $studentid); + if ($submission->get_submission_data()) { + $this->handindatetime = $submission->get_handin_datetime(); + $this->handinstatus = $submission->get_handin_status(); + $this->exportstaff = $submission->get_export_staff(); + } + } + + /** + * Set transfer records. + * @param int $assessmentmappingid + * @param int $studentid + * @return void + * @throws \dml_exception + */ + protected function set_transfer_records(int $assessmentmappingid, int $studentid): void { + $transferlogs = $this->manager->get_transfer_log($assessmentmappingid, $studentid); + if (!empty($transferlogs)) { + foreach ($transferlogs as $log) { + $response = json_decode($log->response); + $result = ($response->code == '0') ? 'success' : 'failed'; + if (is_null($log->errlogid)) { + $errortype = 0; + } else { + $errortype = $log->errortype ?: errormanager::ERROR_UNKNOWN; + } + $timecreated = date('Y-m-d H:i:s', $log->timecreated); + // Get - from request url. + if (preg_match('#moodle/(.*?)/student#', $log->request, $matches)) { + $this->componentgrade = $matches[1]; + } + if ($log->type == manager::PUSH_GRADE) { + $this->lastgradepushresult = $result; + $this->lastgradepusherrortype = $errortype; + $this->lastgradepushtime = $timecreated; + } else if ($log->type == manager::PUSH_SUBMISSION_LOG) { + $this->lastsublogpushresult = $result; + $this->lastsublogpusherrortype = $errortype; + $this->lastsublogpushtime = $timecreated; + } + } + } + } +} diff --git a/classes/output/renderer.php b/classes/output/renderer.php index 1b61b89..5be44de 100644 --- a/classes/output/renderer.php +++ b/classes/output/renderer.php @@ -60,25 +60,30 @@ public function render_link(string $id, string $name, string $url) : string { /** * Render the assessment push status table. * - * @param array $assessmentdata + * @param \stdClass $mapping * @return string * @throws \moodle_exception */ - public function render_assessment_push_status_table(array $assessmentdata) : string { + public function render_assessment_push_status_table(\stdClass $mapping) : string { + $students = null; // Modify the timestamp format and add the label for the last push result. - foreach ($assessmentdata as &$data) { - // Remove the T character in the timestamp. - $data->handin_datetime = str_replace('T', ' ', $data->handin_datetime); - // Add the label for the last push result. - $data->lastgradepushresultlabel = - is_null($data->lastgradepushresult) ? '' : $this->get_label_html($data->lastgradepusherrortype); - // Add the label for the last submission log push result. - $data->lastsublogpushresultlabel = - is_null($data->lastsublogpushresult) ? '' : $this->get_label_html($data->lastsublogpusherrortype); + if (!empty($mapping->students)) { + foreach ($mapping->students as &$data) { + // Remove the T character in the timestamp. + $data->handindatetime = str_replace('T', ' ', $data->handindatetime); + // Add the label for the last push result. + $data->lastgradepushresultlabel = + is_null($data->lastgradepushresult) ? '' : $this->get_label_html($data->lastgradepusherrortype); + // Add the label for the last submission log push result. + $data->lastsublogpushresultlabel = + is_null($data->lastsublogpushresult) ? '' : $this->get_label_html($data->lastsublogpusherrortype); + } + $students = $mapping->students; } return $this->output->render_from_template('local_sitsgradepush/assessmentgrades', [ - 'assessmentdata' => $assessmentdata, + 'tabletitle' => $mapping->formattedname, + 'students' => $students, ]); } diff --git a/classes/submission/submissionfactory.php b/classes/submission/submissionfactory.php index e5e276b..442d06b 100644 --- a/classes/submission/submissionfactory.php +++ b/classes/submission/submissionfactory.php @@ -28,14 +28,22 @@ class submissionfactory { /** * Return submission object. * - * @param \stdClass $coursemodule course module + * @param \int $coursemoduleid course module * @param int $userid user id * @return submission * @throws \coding_exception * @throws \dml_exception * @throws \moodle_exception */ - public static function get_submission(\stdClass $coursemodule, int $userid) { + public static function get_submission(int $coursemoduleid, int $userid) { + // Get course module. + $coursemodule = get_coursemodule_from_id(null, $coursemoduleid); + + // Throw exception if the course module is not found. + if (empty($coursemodule)) { + throw new \moodle_exception('error:coursemodulenotfound', 'local_sitsgradepush', '', $coursemoduleid); + } + switch ($coursemodule->modname) { case 'quiz': return new quiz($coursemodule, $userid); diff --git a/classes/task/adhoctask.php b/classes/task/adhoctask.php index 025e562..723c090 100644 --- a/classes/task/adhoctask.php +++ b/classes/task/adhoctask.php @@ -71,10 +71,17 @@ public function execute() { // Get assessment. $assessment = assessmentfactory::get_assessment($coursemodule); - if ($studentswithgrade = $manager->get_assessment_data($assessment)) { - foreach ($studentswithgrade as $student) { - $manager->push_grade_to_sits($assessment, $student->userid); - $manager->push_submission_log_to_sits($assessment, $student->userid); + if ($assessmentdata = $manager->get_assessment_data($assessment)) { + foreach ($assessmentdata['mappings'] as $mapping) { + // Skip if there is no student in the mapping. + if (empty($mapping->students)) { + continue; + } + // Push grades for each student in the mapping. + foreach ($mapping->students as $student) { + $manager->push_grade_to_sits($mapping, $student->userid); + $manager->push_submission_log_to_sits($mapping, $student->userid); + } } } @@ -85,7 +92,7 @@ public function execute() { $manager->update_push_task_status($task->id, manager::PUSH_TASK_STATUS_COMPLETED); } catch (\Exception $e) { // Log error. - $errlogid = logger::log($e->getMessage()); + $errlogid = logger::log('Push task failed: ' . $e->getMessage()); // Update task status. $manager->update_push_task_status($task->id, manager::PUSH_TASK_STATUS_FAILED, $errlogid); diff --git a/db/access.php b/db/access.php index d5bf1e5..5337930 100644 --- a/db/access.php +++ b/db/access.php @@ -25,17 +25,17 @@ defined('MOODLE_INTERNAL') || die; -$capabilities = array( - 'local/sitsgradepush:mapassessment' => array( +$capabilities = [ + 'local/sitsgradepush:mapassessment' => [ 'riskbitmask' => RISK_CONFIG, 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, - 'archetypes' => array(), - ), - 'local/sitsgradepush:pushgrade' => array( + 'archetypes' => [], + ], + 'local/sitsgradepush:pushgrade' => [ 'riskbitmask' => RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, - 'archetypes' => array(), - ), -); + 'archetypes' => [], + ], +]; diff --git a/db/caches.php b/db/caches.php index 0ed29f4..cef8195 100644 --- a/db/caches.php +++ b/db/caches.php @@ -29,6 +29,6 @@ 'studentspr' => [ 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, - 'simpledata' => true, + 'simpledata' => false, ], ]; diff --git a/db/events.php b/db/events.php index 8f4db77..ead378b 100644 --- a/db/events.php +++ b/db/events.php @@ -24,25 +24,25 @@ */ defined('MOODLE_INTERNAL') || die(); -$observers = array( - array( +$observers = [ + [ 'eventname' => '\mod_assign\event\submission_graded', 'callback' => 'local_sitsgradepush_observer::submission_graded', 'priority' => 200, - ), - array( + ], + [ 'eventname' => '\mod_quiz\event\attempt_submitted', 'callback' => 'local_sitsgradepush_observer::quiz_attempt_submitted', 'priority' => 200, - ), - array( + ], + [ 'eventname' => '\core\event\user_graded', 'callback' => 'local_sitsgradepush_observer::user_graded', 'priority' => 200, - ), - array( + ], + [ 'eventname' => '\mod_quiz\event\attempt_regraded', 'callback' => 'local_sitsgradepush_observer::quiz_attempt_regraded', 'priority' => 200, - ), -); + ], +]; diff --git a/db/install.xml b/db/install.xml index a0d787f..3ca4204 100644 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -49,7 +49,7 @@ - + diff --git a/db/services.php b/db/services.php index ce061c8..777c8d6 100644 --- a/db/services.php +++ b/db/services.php @@ -27,13 +27,13 @@ // We defined the web service functions to install. $functions = [ - 'local_sitsgradepush_schedule_push_task' => array( + 'local_sitsgradepush_schedule_push_task' => [ 'classname' => 'local_sitsgradepush\external\schedule_push_task', 'description' => 'Schedule a push task', 'ajax' => true, 'type' => 'write', - 'loginrequired' => true - ), + 'loginrequired' => true, + ], ]; diff --git a/db/tasks.php b/db/tasks.php index 8391447..b9ad1e4 100644 --- a/db/tasks.php +++ b/db/tasks.php @@ -25,14 +25,14 @@ defined('MOODLE_INTERNAL') || die; -$tasks = array( - array( +$tasks = [ + [ 'classname' => 'local_sitsgradepush\task\pushtask', 'blocking' => 0, 'minute' => '*', 'hour' => '*', 'day' => '*', 'month' => '*', - 'dayofweek' => '*' - ) -); + 'dayofweek' => '*', + ], +]; diff --git a/db/upgrade.php b/db/upgrade.php index 18c1a2d..d598e55 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -262,5 +262,45 @@ function xmldb_local_sitsgradepush_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2023060500, 'local', 'sitsgradepush'); } + if ($oldversion < 2023103100) { + + // Define field assessmentmappingid to be added to local_sitsgradepush_tfr_log. + $table = new xmldb_table('local_sitsgradepush_tfr_log'); + $field = new xmldb_field( + 'assessmentmappingid', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'coursemoduleid'); + + // Conditionally launch add field assessmentmappingid. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Patch transfer log. + $DB->execute(' + UPDATE {local_sitsgradepush_tfr_log} t + SET t.assessmentmappingid = + (SELECT m.id FROM {local_sitsgradepush_mapping} m WHERE m.coursemoduleid = t.coursemoduleid)'); + + // Launch change of nullability for field assessmentmappingid. + $dbman->change_field_notnull($table, $field); + + // Sitsgradepush savepoint reached. + upgrade_plugin_savepoint(true, 2023103100, 'local', 'sitsgradepush'); + } + + if ($oldversion < 2023110600) { + + // Define field coursemoduleid to be dropped from local_sitsgradepush_tfr_log. + $table = new xmldb_table('local_sitsgradepush_tfr_log'); + $field = new xmldb_field('coursemoduleid'); + + // Conditionally launch drop field coursemoduleid. + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + // Sitsgradepush savepoint reached. + upgrade_plugin_savepoint(true, 2023110600, 'local', 'sitsgradepush'); + } + return true; } diff --git a/index.php b/index.php index 7d8b5e0..8cbaa61 100644 --- a/index.php +++ b/index.php @@ -53,8 +53,6 @@ // Check user's capability. require_capability('local/sitsgradepush:pushgrade', $context); -$course = get_course($coursemodule->course); - // Set the required data into the PAGE object. $param = ['id' => $coursemoduleid]; $url = new moodle_url('/local/sitsgradepush/index.php', $param); @@ -65,9 +63,6 @@ $PAGE->set_title('SITS Grade Push'); $PAGE->activityheader->disable(); -// Get assessment. -$assessment = assessmentfactory::get_assessment($coursemodule); - // Set the breadcrumbs. $PAGE->navbar->add('SITS Grade Push', new moodle_url('/local/sitsgradepush/index.php', $param)); @@ -77,17 +72,23 @@ // Get renderer. $renderer = $PAGE->get_renderer('local_sitsgradepush'); -$manager = manager::get_manager(); echo '
'; // Assessment name. -echo '

Sits grade push

'; -// Get students with grades. -$studentswithgrade = $manager->get_assessment_data($assessment); +echo '

Sits grade push history

'; -if (!empty($studentswithgrade)) { +$manager = manager::get_manager(); +// Get assessment. +$assessment = assessmentfactory::get_assessment($coursemodule); + +// Get page content. +$content = $manager->get_assessment_data($assessment); + +if (!empty($content)) { // Check if asynchronous grade push is enabled. $async = get_config('local_sitsgradepush', 'async'); + + // Check if this course module has pending task. if ($async) { // Get push button label. $buttonlabel = get_string('label:pushgrade', 'local_sitsgradepush'); @@ -101,14 +102,18 @@ } else { // Push grade and submission log. if ($pushgrade == 1) { - // Push grades. - foreach ($studentswithgrade as $student) { - $manager->push_grade_to_sits($assessment, $student->userid); - $manager->push_submission_log_to_sits($assessment, $student->userid); + // Loop through each mapping. + foreach ($content['mappings'] as $mapping) { + // Push grades for each student in the mapping. + foreach ($mapping->students as $student) { + $manager->push_grade_to_sits($mapping, $student->userid); + $manager->push_submission_log_to_sits($mapping, $student->userid); + } } - // Refresh data after completed all pushes. - $studentswithgrade = $manager->get_assessment_data($assessment); - $buttonlabel = get_string('label:ok', 'local_sitsgradepush');; + + // Refresh data after completed all pushes. + $content = $manager->get_assessment_data($assessment); + $buttonlabel = get_string('label:ok', 'local_sitsgradepush'); } else { $url->param('pushgrade', 1); $buttonlabel = get_string('label:pushgrade', 'local_sitsgradepush'); @@ -129,17 +134,24 @@ 'local_sitsgradepush', [ 'statustext' => $lastfinishedtask->statustext, 'date' => date('d/m/Y', $lastfinishedtask->timeupdated), - 'time' => date('g:i:s a', $lastfinishedtask->timeupdated)]) . + 'time' => date('g:i:s a', $lastfinishedtask->timeupdated), ]) . '

'; } } else { echo '

' . get_string('error:assessmentisnotmapped', 'local_sitsgradepush') . '

'; } - // Render assessment push status table. - echo $renderer->render_assessment_push_status_table($studentswithgrade); + // Display grade push records for each mapping. + foreach ($content['mappings'] as $mapping) { + echo $renderer->render_assessment_push_status_table($mapping); + } + + // Display invalid students. + if (!empty($content['invalidstudents']->students)) { + echo $renderer->render_assessment_push_status_table($content['invalidstudents']); + } } else { - echo '

' . get_string('error:nostudentgrades', 'local_sitsgradepush') . '

'; + echo '

' . get_string('error:assessmentisnotmapped', 'local_sitsgradepush') . '

'; } echo '
'; diff --git a/lang/en/local_sitsgradepush.php b/lang/en/local_sitsgradepush.php index 1958df9..4b824e8 100644 --- a/lang/en/local_sitsgradepush.php +++ b/lang/en/local_sitsgradepush.php @@ -54,25 +54,33 @@ $string['label:pushgrade'] = 'Push grades'; $string['label:ok'] = 'OK'; $string['label:lastpushtext'] = 'Last scheduled push task {$a->statustext} {$a->date} at {$a->time}'; +$string['option:none'] = 'NONE'; $string['gradepushassessmentselect'] = 'Select SITS assessment'; $string['gradepushassessmentselect_help'] = 'Select SITS assessment to link to this activity.'; $string['reassessmentselect'] = 'Re-assessment'; $string['reassessmentselect_help'] = 'Select YES if it is a re-assessment.'; $string['subplugintype_sitsapiclient'] = 'API client used for data integration.'; $string['cachedef_studentspr'] = 'Student\'s SPR code per SITS assessment pattern'; +$string['invalidstudents'] = 'Students not valid for the mapped assessment components'; // Error strings. $string['error:assessmentmapping'] = 'No valid mapping or component grade. {$a}'; $string['error:assessmentisnotmapped'] = 'This activity is not mapped to any assessment component.'; -$string['error:gradecomponentmapped'] = 'This component grade had been mapped to another activity.'; +$string['error:componentgradepushed'] = '{$a} cannot be removed because it has grade push records.'; +$string['error:componentgrademapped'] = '{$a} had been mapped to another activity.'; $string['error:pastactivity'] = 'It looks like this course is from a previous academic year, mappings are not allowed.'; $string['error:mapassessment'] = 'You do not have permission to map assessment.'; $string['error:nostudentgrades'] = 'No student grades found.'; +$string['error:nostudentfoundformapping'] = 'No student found for this assessment component.'; $string['error:emptyresponse'] = 'Empty response received when calling {$a}.'; $string['error:turnitin_numparts'] = 'Turnitin assignment with multiple parts is not supported by Grade Push.'; $string['error:duplicatedtask'] = 'There is already a push task in queue / processing for this course module.'; $string['error:coursemodulenotfound'] = 'Course module not found.'; $string['error:tasknotfound'] = 'Push task not found.'; +$string['error:multiplemappingsnotsupported'] = 'Multiple assessment component mappings is not supported by {$a}'; +$string['error:studentnotfound'] = 'Student with idnumber {$a->idnumber} not found for component grade {$a->componentgrade}'; +$string['error:coursemodulenotfound'] = 'Course module not found. ID: {$a}'; +$string['error:duplicatemapping'] = 'Cannot map multiple assessment components with same module delivery to an activity. Mapcode: {$a}'; $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 Grade Push.'; diff --git a/lib.php b/lib.php index a116a4d..91fb912 100644 --- a/lib.php +++ b/lib.php @@ -39,12 +39,14 @@ function local_sitsgradepush_coursemodule_standard_elements($formwrapper, $mform if (get_config('local_sitsgradepush', 'enabled') !== '1') { return; } + // Do not show settings if user does not have the capability. if (!has_capability( 'local/sitsgradepush:mapassessment', context_course::instance($formwrapper->get_course()->id))) { return; } + $manager = manager::get_manager(); // Display settings for certain type of activities only. $modulename = $formwrapper->get_current()->modulename; @@ -52,21 +54,29 @@ function local_sitsgradepush_coursemodule_standard_elements($formwrapper, $mform // Add setting header. $mform->addElement('header', 'gradepushheader', 'Grade Push'); - // Add component grade dropdown list. + // Autocomplete options. + $options = [ + 'multiple' => true, + 'noselectionstring' => get_string('option:none', 'local_sitsgradepush'), + ]; + + // Add autocomplete element to form. $select = $mform->addElement( - 'select', + 'autocomplete', 'gradepushassessmentselect', get_string('label:gradepushassessmentselect', 'local_sitsgradepush'), - ['0' => 'NONE'] + [], + $options ); - $mform->setType('gradepushassessmentselect', PARAM_INT); - $mform->addHelpButton('gradepushassessmentselect', 'gradepushassessmentselect', 'local_sitsgradepush'); // Add component grades options to the dropdown list. - $manager = manager::get_manager(); if (empty($manager->get_api_errors())) { // Get component grade options. - $options = $manager->get_component_grade_options($formwrapper->get_course()->id); + $options = $manager->get_component_grade_options( + $formwrapper->get_course()->id, + $formwrapper->get_current()->coursemodule + ); + if (empty($options)) { $mform->addElement( 'html', @@ -74,38 +84,23 @@ function local_sitsgradepush_coursemodule_standard_elements($formwrapper, $mform ); } else { foreach ($options as $option) { - $select->addOption($option->text, $option->value, $option->disabled); + $select->addOption($option->text, $option->value, [$option->selected]); } - // If it's a Turnitin assignment and more than one part, disable the dropdown list. + // Notify user multiple parts Turnitin assignment is not supported by Grade Push. if ($modulename === 'turnitintooltwo') { $mform->addElement( 'html', "

". get_string('form:info_turnitin_numparts', 'local_sitsgradepush') ."

" ); - $mform->disabledIf('gradepushassessmentselect', 'numparts', 'gt', 1); } - if ($cm = $formwrapper->get_coursemodule()) { - $disableselect = false; - // Disable the settings if this activity is already mapped. - if ($assessmentmapping = $manager->get_assessment_mapping($cm->id)) { - $select->setSelected($assessmentmapping->componentgradeid); - $disableselect = true; - } else { - // Disable the settings if this activity is not in the current academic year. - if (!$manager->is_current_academic_year_activity($formwrapper->get_course()->id)) { - $mform->addElement( - 'html', - "

" . get_string('error:pastactivity', 'local_sitsgradepush') . "

" - ); - $disableselect = true; - } - } - - if ($disableselect) { - $select->updateAttributes(['disabled' => 'disabled']); - } + // Notify user this activity is not in the current academic year. + if (!$manager->is_current_academic_year_activity($formwrapper->get_course()->id)) { + $mform->addElement( + 'html', + "

" . get_string('error:pastactivity', 'local_sitsgradepush') . "

" + ); } } } @@ -140,10 +135,7 @@ function local_sitsgradepush_coursemodule_standard_elements($formwrapper, $mform */ function local_sitsgradepush_coursemodule_edit_post_actions($data, $course) { $manager = manager::get_manager(); - // Save assessment mapping. - if (!empty($data->gradepushassessmentselect)) { - $manager->save_assessment_mapping($data); - } + $manager->save_assessment_mappings($data); return $data; } @@ -161,18 +153,24 @@ function local_sitsgradepush_coursemodule_validation($fromform, $fields) { // Extract activity type from form class name e.g. assign, quiz etc. $activitytype = explode('_', get_class($fromform)); - // Run check for component grade for unmapped activity only. - if (!$manager->is_activity_mapped($fields['coursemodule'])) { - // Check if the component grade has been mapped to another activity. - if (in_array($activitytype[1], $manager->get_allowed_activities()) && !empty($fields['gradepushassessmentselect'])) { - if ($manager->is_component_grade_mapped($fields['gradepushassessmentselect'])) { - return ['gradepushassessmentselect' => get_string('error:gradecomponentmapped', 'local_sitsgradepush')]; - } - } + // Exit if the activity type is not allowed. + if (!in_array($activitytype[1], $manager->get_allowed_activities())) { + return; + } + + // Remove any empty values. + $componentgrades = array_filter($fields['gradepushassessmentselect']); + + // Validate component grades and return any error message. + // Course module id is empty if this is a new activity. + $coursemoduleid = $fields['coursemodule'] ?? null; + $result = $manager->validate_component_grades($componentgrades, $coursemoduleid); + if ($result->errormessages) { + return ['gradepushassessmentselect' => implode('
', $result->errormessages)]; } // For Turnitin assignment, check if the number of parts is greater than 1. - if ($activitytype[1] === 'turnitintooltwo' && !empty($fields['gradepushassessmentselect'])) { + if ($activitytype[1] === 'turnitintooltwo' && !empty($componentgrades)) { if ($fields['numparts'] > 1) { return ['numparts' => get_string('error:turnitin_numparts', 'local_sitsgradepush')]; } @@ -208,10 +206,10 @@ function local_sitsgradepush_extend_settings_navigation(settings_navigation $set } // Build the grade push page url. - $url = new moodle_url('/local/sitsgradepush/index.php', array( + $url = new moodle_url('/local/sitsgradepush/index.php', [ 'id' => $cm->id, - 'modname' => $cm->modname - )); + 'modname' => $cm->modname, + ]); // Create the node. $node = navigation_node::create( diff --git a/settings.php b/settings.php index 542aad1..a6ecb40 100644 --- a/settings.php +++ b/settings.php @@ -108,7 +108,7 @@ )); // Set the user profile field for export staff's source. - $options = array(); + $options = []; $manager = manager::get_manager(); $fields = $manager->get_user_profile_fields(); if (!empty($fields)) { @@ -117,11 +117,11 @@ } } $settings->add(new admin_setting_configselect( - 'local_sitsgradepush/user_profile_field', - get_string('settings:userprofilefield', 'local_sitsgradepush'), - get_string('settings:userprofilefield:desc', 'local_sitsgradepush'), - '', - $options) + 'local_sitsgradepush/user_profile_field', + get_string('settings:userprofilefield', 'local_sitsgradepush'), + get_string('settings:userprofilefield:desc', 'local_sitsgradepush'), + '', + $options) ); } diff --git a/templates/assessmentgrades.mustache b/templates/assessmentgrades.mustache index 9aef29f..04d39dd 100644 --- a/templates/assessmentgrades.mustache +++ b/templates/assessmentgrades.mustache @@ -25,14 +25,14 @@ * none Context variables required for this template: - * assessmentname - The assessment name - * assessmentdata - Array variable, array of assessment data + * tabletitle - The assessment name + * students - Array variable, array of students Example context (json): - {"assessmentname":"","assessmentdata":[]} + {"tabletitle":"","students":[]} }} - + @@ -44,17 +44,19 @@ - {{#assessmentdata}} + {{#students}} - + - {{/assessmentdata}} + {{/students}}
Grade push history table{{tabletitle}}
Student
{{firstname}} {{lastname}} {{idnumber}} {{marks}}{{handin_datetime}}{{handindatetime}} {{{lastgradepushresultlabel}}}{{lastgradepushtime}} {{{lastsublogpushresultlabel}}}{{lastsublogpushtime}}
- +{{^students}} +

{{#str}} error:nostudentfoundformapping, local_sitsgradepush {{/str}}

+{{/students}} \ No newline at end of file diff --git a/tests/privacy_provider_test.php b/tests/privacy_provider_test.php index 4360920..0d42c40 100644 --- a/tests/privacy_provider_test.php +++ b/tests/privacy_provider_test.php @@ -27,6 +27,7 @@ * @author Alex Yeung */ class privacy_provider_test extends \advanced_testcase { + protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); @@ -39,7 +40,7 @@ protected function setUp(): void { * @return void * @throws \coding_exception */ - public function test_get_reason() { + public function test_get_reason(): void { $reason = get_string(provider::get_reason(), 'local_sitsgradepush'); $this->assertEquals('This plugin does not store any personal data.', $reason); } diff --git a/version.php b/version.php index c0401cd..d5b4663 100644 --- a/version.php +++ b/version.php @@ -27,10 +27,10 @@ $plugin->component = 'local_sitsgradepush'; $plugin->release = '0.1.0'; -$plugin->version = 2023060500; +$plugin->version = 2023110600; $plugin->requires = 2021051708; $plugin->maturity = MATURITY_ALPHA; -$plugin->dependencies = array( +$plugin->dependencies = [ 'block_portico_enrolments' => 2023012400, - 'block_lifecycle' => 2022120800 -); + 'block_lifecycle' => 2022120800, +];