diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 87cf412..686c4bb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @rakeshprabhu @harish-talview @merlano17 @devang1281 @manquer @KeerthiHarish +* @rakeshprabhu @harish-talview @merlano17 @devang1281 @KeerthiHarish diff --git a/classes/local/api/tracker.php b/classes/local/api/tracker.php index 6dfd6c4..bb40278 100644 --- a/classes/local/api/tracker.php +++ b/classes/local/api/tracker.php @@ -45,7 +45,51 @@ class tracker * @return void As the insertion is done through the {js} template API. */ - private static function generate_auth_token($api_base_url, $payload) + public static function fetchSecureToken($external_session_id, $external_attendee_id) + { + $api_base_url = trim(get_config('quizaccess_proctor', 'proview_callback_url')); + $auth_payload = new \stdClass(); + $auth_payload->username = trim(get_config('quizaccess_proctor', 'proview_admin_username')); + $auth_payload->password = trim(get_config('quizaccess_proctor', 'proview_admin_password')); + $auth_response = self::generate_auth_token($api_base_url, $auth_payload); + $auth_token = json_decode($auth_response)->access_token; + $proctor_token = trim(get_config('local_proview', 'token')); + $url = $api_base_url . '/token/playback'; + $data = array( + 'proctor_token' => $proctor_token, + 'validity' => 120, + 'external_session_id' => $external_session_id, + 'external_attendee_id' => $external_attendee_id + ); + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Content-Type: application/json', + 'Authorization: Bearer ' . $auth_token + )); + $response = curl_exec($ch); + curl_close($ch); + return json_decode($response, true); + } + + public static function storeFallbackDetails($attempt_no, $proview_url, $proctor_type, $user_id, $quiz_id) + { + global $DB; + $response = $DB->insert_record('local_proview', [ + "quiz_id" => $quiz_id, + "proview_url" => $proview_url, + "user_id" => $user_id, + "attempt_no" => $attempt_no, + "proctor_type" => $proctor_type, + ]); + return $response; + } + + + +private static function generate_auth_token($api_base_url, $payload) { $curl = curl_init(); curl_setopt_array($curl, [ @@ -78,26 +122,25 @@ private static function generate_auth_token($api_base_url, $payload) } } - - private static function capture_error(\Throwable $err) + private static function capture_error(\Throwable $err) { \Sentry\init(['dsn' => 'https://61facdc5414c4c73ab2b17fe902bf9ba@o286634.ingest.sentry.io/5304587']); \Sentry\captureException($err); } + public static function insert_tracking() { global $PAGE, $OUTPUT, $USER, $DB; $pageinfo = get_context_info_array($PAGE->context->id); $template = new stdClass(); - $template->proview_url = get_config('local_proview', 'proview_url'); - $template->token = get_config('local_proview', 'token'); - $template->enabled = get_config('local_proview', 'enabled'); - $template->root_dir = get_config('local_proview', 'root_dir'); + $template->proview_url = trim(get_config('local_proview', 'proview_url')); + $template->token = trim(get_config('local_proview', 'token')); + $template->enabled = trim(get_config('local_proview', 'enabled')); + $template->root_dir = trim(get_config('local_proview', 'root_dir')); $template->profile_id = $USER->id; - $template->proview_callback_url = get_config('quizaccess_proctor', 'proview_callback_url'); - - + $template->proview_callback_url = trim(get_config('quizaccess_proctor', 'proview_callback_url')); + $template->proview_playback_url = trim(get_config('local_proview', 'proview_playback_url')); $cm = $PAGE->cm; if ($cm && $cm->instance) { $quiz = $DB->get_record('quiz', array('id' => $cm->instance)); // Fetching current quiz data for password. @@ -105,6 +148,7 @@ public static function insert_tracking() $template->quiz_id = $quiz->id; $attempt = $DB->get_record('quiz_attempts', array('quiz' => $quiz->id, 'userid' => $USER->id, 'state' => 'inprogress')); + if (!$attempt) { $attempts = $DB->get_records('quiz_attempts', array('quiz' => $quiz->id, 'userid' => $USER->id)); $attempt = $attempts ? max(array_filter(array_column($attempts, 'attempt'))) : 0; @@ -113,20 +157,16 @@ public static function insert_tracking() $attempt = $attempt->attempt; } $template->current_attempt = $attempt; - $api_base_url = trim(get_config('quizaccess_proctor', 'proview_callback_url')); - $auth_payload = new \stdClass(); - $auth_payload->username = trim(get_config('quizaccess_proctor', 'proview_admin_username')); - $auth_payload->password = trim(get_config('quizaccess_proctor', 'proview_admin_password')); - $auth_response = self::generate_auth_token($api_base_url, $auth_payload); - $template->auth_token = json_decode($auth_response)->access_token; if (strpos($PAGE->url, ('mod/quiz/report'))) { - $attempts = $DB->get_records('local_proview', array('quiz_id' => $quiz->id), 'attempt_no', 'attempt_no,proview_url,quiz_id,user_id,proctor_type'); - foreach ($attempts as $attempt) { - $noOfAttempts=$DB->get_records('quiz_attempts', array('id' => $attempt->attempt_no)); - $attempt->attempt_no = $noOfAttempts ? max(array_filter(array_column($noOfAttempts, 'attempt'))) : 0; + $quiz_attempts = $DB->get_records('quiz_attempts', array('quiz' => $quiz->id)); + foreach ($quiz_attempts as $quiz_attempt) { + $local_proview_data = $DB->get_record('local_proview', array('quiz_id' => $quiz->id, 'attempt_no' => $quiz_attempt->id), 'proview_url,proctor_type,attempt_no'); + $quiz_attempt->proview_url = isset($local_proview_data->proview_url) ? $local_proview_data->proview_url : ''; + $quiz_attempt->proctor_type = isset($local_proview_data->proctor_type) ? $local_proview_data->proctor_type : $DB->get_record('quizaccess_proctor', array('quizid' => $quiz->id), 'proctortype')->proctortype; + $quiz_attempt->attempt_no = $quiz_attempt->attempt; } - $template->attempts = json_encode($attempts); + $template->attempts = json_encode($quiz_attempts); } } if ($pageinfo && !empty($template->token)) { @@ -134,7 +174,6 @@ public static function insert_tracking() // the output; only that the $PAGE->requires are filled. $OUTPUT->render_from_template('local_proview/tracker', $template); } - } } diff --git a/fetch-secure-token.php b/fetch-secure-token.php new file mode 100644 index 0000000..47982b7 --- /dev/null +++ b/fetch-secure-token.php @@ -0,0 +1,14 @@ + 'Invalid action']); +} diff --git a/store-fallback-details.php b/store-fallback-details.php new file mode 100644 index 0000000..1388935 --- /dev/null +++ b/store-fallback-details.php @@ -0,0 +1,17 @@ + 'Invalid action']); +} diff --git a/templates/tracker.mustache b/templates/tracker.mustache index 8b28bb9..aded886 100644 --- a/templates/tracker.mustache +++ b/templates/tracker.mustache @@ -129,13 +129,13 @@ let node = document.createElement("A"); node.setAttribute("href", ""); - let parentNode = document.createElement("TH"); + let parentNode = document.createElement("TH"); parentNode.setAttribute("scope", "col"); parentNode.setAttribute("class", `header c${thead.firstChild.children.length}`); let textnode = document.createTextNode("Proview URL"); - node.appendChild(textnode); + node.appendChild(textnode); parentNode.appendChild(node); thead.firstChild.appendChild(parentNode); for (let i = 0; i < tbody.children.length; i++) { @@ -148,46 +148,74 @@ tbody.children[i+1].appendChild(emptyNode); break; } + let external_session_id=''; + const quiz_id=attempts[attempt_id].quiz; + const user_id=attempts[attempt_id].userid; + const attempt_no=attempts[attempt_id].attempt_no; + const external_attendee_id=user_id; textnode = document.createTextNode(attempts[attempt_id]&&"Proctor link"||""); - node = document.createElement("a"); + if(attempts[attempt_id]&&attempts[attempt_id].proview_url===''){ + node = document.createElement("a"); + node.appendChild(document.createTextNode("Resync ")); + node.setAttribute("title", "Resync "); + node.setAttribute("class", "btn btn-warning btn-sm"); + node.addEventListener("click", () => { + const proctor_type = attempts[attempt_id].proctor_type; + if(proctor_type=='ai_proctor'||proctor_type=='record_and_review'||proctor_type=='live_proctor'){ + processSecureToken(proctor_type, quiz_id, user_id, attempt_no, external_attendee_id) + .then(function(response) { + console.log("Playback details:", response); + const attempt = attempts[attempt_id].id; + const session_uuid = response.session_uuid; + const proview_url = `{{proview_playback_url}}` + '/' + session_uuid; + attempts[attempt_id].proview_url = proview_url; + storeFallbackDetails(quiz_id, user_id, attempt ,proview_url, proctor_type) + .then(function(response){ + console.log("Fallback details stored:", response); + location.reload(); + }) + .catch(function(error) { + console.error("Error:", error); + + }); + }) + .catch(function(error) { + console.error("Error:", error); + }); + } + }); + } + else{ + node = document.createElement("a"); node.setAttribute("title", "Proview Link"); node.setAttribute("href", attempts[attempt_id]&&attempts[attempt_id].proview_url||""); node.setAttribute("target", "proview"); node.addEventListener("click",()=>{ const proctor_type = attempts[attempt_id].proctor_type; - if(proctor_type=='ai_proctor'||proctor_type=='record_and_review'||proctor_type=='live_proctor'){ - auth_token =`{{auth_token}}`; - let external_session_id=''; - const quiz_id=attempts[attempt_id].quiz_id; - const user_id=attempts[attempt_id].user_id; - const attempt_no=attempts[attempt_id].attempt_no; - const external_attendee_id=user_id; - if(proctor_type=='ai_proctor'||proctor_type=='record_and_review'){ - external_session_id= quiz_id+'-'+user_id+'-'+attempt_no; - } - else if(proctor_type=='live_proctor'){ - external_session_id= quiz_id+'-'+user_id; - } - const proctor_token= '{{token}}'; - const token=fetchSecureToken(proctor_token,external_session_id,external_attendee_id,auth_token); - token.then((token)=>{ - const url=attempts[attempt_id].proview_url; - table.setAttribute("style","display: none"); - modal.setAttribute("style","display: block"); - iframe.setAttribute("src", url + '/?token=' + token.token); + if(proctor_type=='ai_proctor'||proctor_type=='record_and_review'||proctor_type=='live_proctor'){ + processSecureToken(proctor_type, quiz_id, user_id, attempt_no, external_attendee_id) + .then(function(response) { + console.log("Secure token:", response.token); + url=attempts[attempt_id].proview_url; + iframe.setAttribute("src", url + '/?token=' + response.token); + }) + .catch(function(error) { + console.error("Error:", error); }); - } - }) + table.setAttribute("style","display: none"); + modal.setAttribute("style","display: block"); + } }); } node.appendChild(textnode); - parentNode = document.createElement("TD"); + parentNode = document.createElement("TD"); parentNode.setAttribute("id", `mod-quiz-report-overview-report_r${i}_c${tbody.children[i].children.length}`); parentNode.setAttribute("class", `cell c${tbody.children[i].children.length}`); - parentNode.appendChild(node); + parentNode.appendChild(node); tbody.children[i].appendChild(parentNode); - } + +} {{! Logic to append Proview Url Column in Attempts table in moodle UI ( -> Settings -> Grades (Nested under Results)), ENDS }} - {{! Logic to display Proview admin in iframe and hide the attempts table (and vice-versa) STARTS }} + {{! Logic to display Proview admin in iframe and hide the attempts table (and vice-versa) STARTS }} var modal = document.createElement('div'); modal.setAttribute("id","modal"); modal.setAttribute("name","modal"); @@ -201,7 +229,7 @@ var close = document.createElement('div'); close.setAttribute("style","width: 100%;"); close.setAttribute("align","left"); - close.innerHTML ='x'; + close.innerHTML ='X'; close.addEventListener("click", ()=>{ table.setAttribute("style","display: block"); modal.setAttribute("style","display: none"); @@ -210,7 +238,7 @@ modal.appendChild(close); modal.appendChild(iframe); table.parentNode.appendChild(modal); - {{! Logic to display Proview admin in iframe and hide the attempts table (and vice-versa) ENDS }} + {{! Logic to display Proview admin in iframe and hide the attempts table (and vice-versa) ENDS }} } if(current.match('mod/quiz/(attempt|summary)') && window.self != window.top) { @@ -219,24 +247,59 @@ if(!current.match('mod/quiz/(attempt|summary|startattempt|view)') && window.self != window.top ) { parent.postMessage({type: 'stopProview',url: window.location.href}, childOrigin); } - function fetchSecureToken(proctor_token,external_session_id,external_attendee_id,auth_token){ - const url = `{{proview_callback_url}}`+'/token/playback'; - const data = { - proctor_token: proctor_token, - validity: 120, - external_session_id: external_session_id, - external_attendee_id: external_attendee_id - }; - const options = { - method: 'POST', - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ` +auth_token - } - }; - return fetch(url, options) - .then(response => response.json()) + function fetchSecureToken(external_session_id, external_attendee_id) { + return new Promise(function(resolve, reject) { + $.ajax({ + type: "POST", + url: "{{root_dir}}local/proview/fetch-secure-token.php", + data: { + action: "fetch_secure_token", + external_session_id: external_session_id, + external_attendee_id: external_attendee_id, + }, + dataType: "json", + success: function (response) { + resolve(response); + }, + error: function (error) { + console.error("Error fetching secure token:", error); + reject(error); + } + }); + }); + } + function storeFallbackDetails(quiz_id, user_id, attempt_no, proview_url, proctor_type) { + return new Promise(function(resolve, reject) { + $.ajax({ + type: "POST", + url: "{{root_dir}}local/proview/store-fallback-details.php", + data: { + action: "store_fallback_details", + quiz_id: quiz_id, + user_id: user_id, + attempt_no: attempt_no, + proview_url: proview_url, + proctor_type: proctor_type + }, + dataType: "json", + success: function (response) { + resolve(response); + }, + error: function (error) { + console.error("Error storing fallback details:", error); + reject(error); + } + }); + }); + } + function processSecureToken(proctor_type, quiz_id, user_id, attempt_no, external_attendee_id){ + if(proctor_type=='ai_proctor'||proctor_type=='record_and_review'){ + external_session_id= quiz_id+'-'+user_id+'-'+attempt_no; + } + else if(proctor_type=='live_proctor'){ + external_session_id= quiz_id+'-'+user_id; + } + return (fetchSecureToken(external_session_id, external_attendee_id)); } {{/enabled}} -{{/js}} \ No newline at end of file +{{/js}} diff --git a/version.php b/version.php index a32ccb1..293b63a 100644 --- a/version.php +++ b/version.php @@ -28,9 +28,9 @@ defined('MOODLE_INTERNAL') || die; -$plugin->version = 2023091202; +$plugin->version = 2023092501; $plugin->requires = 2020061500; -$plugin->release = '3.2.0 (Build: 2023091202)'; +$plugin->release = '3.2.0 (Build: 2023092501)'; $plugin->maturity = MATURITY_STABLE; $plugin->component = 'local_proview';