-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fallback for proctorlink #70
Changes from 16 commits
5f3d336
bd7fb03
de7ba35
5150866
2c71232
11b1591
987c742
9950f1e
7ee4c16
10cdcbb
a9ffc2a
c54c979
5a15003
93b5e78
4cb9796
2a3faf6
50d0f41
d5d1aa1
ceb4d9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
* @rakeshprabhu @harish-talview @merlano17 @devang1281 @manquer @KeerthiHarish | ||
* @rakeshprabhu @harish-talview @merlano17 @devang1281 @KeerthiHarish |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,7 +45,53 @@ 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; | ||
$base_url = get_config('quizaccess_proctor', 'proview_callback_url'); | ||
$proctor_token = get_config('local_proview', 'token'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rohansharmasitoula Please trim the values taken from config |
||
$url = $base_url . '/token/playback'; | ||
|
||
$data = array( | ||
'proctor_token' => $proctor_token, | ||
'validity' => 120, | ||
'external_session_id' => $external_session_id, | ||
'external_attendee_id' => $external_attendee_id | ||
); | ||
$options = array( | ||
'http' => array( | ||
'method' => 'POST', | ||
'header' => "Content-Type: application/json\r\n" . | ||
"Authorization: Bearer " . $auth_token, | ||
'content' => json_encode($data) | ||
) | ||
); | ||
$context = stream_context_create($options); | ||
$response = file_get_contents($url, false, $context); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rohansharmasitoula What is the benefit of using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using file_get_contents seemed to be easier than Curl, was unaware about the pros of curl. Changed to curl implementation. |
||
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,12 +124,12 @@ 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://[email protected]/5304587']); | ||
\Sentry\captureException($err); | ||
} | ||
|
||
public static function insert_tracking() | ||
{ | ||
global $PAGE, $OUTPUT, $USER, $DB; | ||
|
@@ -96,15 +142,15 @@ public static function insert_tracking() | |
$template->root_dir = get_config('local_proview', 'root_dir'); | ||
$template->profile_id = $USER->id; | ||
$template->proview_callback_url = get_config('quizaccess_proctor', 'proview_callback_url'); | ||
|
||
|
||
$template->proview_playback_url = 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. | ||
$template->quiz_password = $quiz->password; | ||
$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,28 +159,28 @@ 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)) { | ||
// The templates only contains a "{js}" block; so we don't care about | ||
// the output; only that the $PAGE->requires are filled. | ||
$OUTPUT->render_from_template('local_proview/tracker', $template); | ||
} | ||
|
||
} | ||
} | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rohansharmasitoula Why are empty lines added at EOF There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed. |
||
|
||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?php | ||
use local_proview\local\api\tracker; | ||
require_once('../../config.php'); | ||
global $CFG; | ||
if (isset($_POST['action']) && $_POST['action'] === 'fetch_secure_token') { | ||
$external_session_id = $_POST['external_session_id']; | ||
$external_attendee_id = $_POST['external_attendee_id']; | ||
$token_response = tracker::fetchSecureToken( $external_session_id, $external_attendee_id); | ||
header('Content-Type: application/json'); | ||
echo json_encode($token_response); | ||
} else { | ||
http_response_code(400); | ||
echo json_encode(['error' => 'Invalid action']); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
use local_proview\local\api\tracker; | ||
require_once('../../config.php'); | ||
global $CFG; | ||
if (isset($_POST['action']) && $_POST['action'] === 'store_fallback_details') { | ||
$attempt_no = $_POST['attempt_no']; | ||
$proview_url = $_POST['proview_url']; | ||
$proctor_type = $_POST['proctor_type']; | ||
$user_id = $_POST['user_id']; | ||
$quiz_id = $_POST['quiz_id']; | ||
$response = tracker::storeFallbackDetails($attempt_no, $proview_url, $proctor_type, $user_id, $quiz_id); | ||
header('Content-Type: application/json'); | ||
echo json_encode($response); | ||
} else { | ||
http_response_code(400); | ||
echo json_encode(['error' => 'Invalid action']); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,87 @@ | |
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'){ | ||
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; | ||
} | ||
fetchSecureToken(external_session_id, external_attendee_id) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rohansharmasitoula This can be written as a function and reused There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. refactored |
||
.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'||proctor_type=='live_proctor'){ | ||
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); | ||
|
||
fetchSecureToken(external_session_id, 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 (<Quiz> -> 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 +242,7 @@ | |
var close = document.createElement('div'); | ||
close.setAttribute("style","width: 100%;"); | ||
close.setAttribute("align","left"); | ||
close.innerHTML ='<span style="cursor: pointer">x</span>'; | ||
close.innerHTML ='<span style="cursor: pointer">X</span>'; | ||
close.addEventListener("click", ()=>{ | ||
table.setAttribute("style","display: block"); | ||
modal.setAttribute("style","display: none"); | ||
|
@@ -210,7 +251,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 +260,50 @@ | |
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); | ||
} | ||
}); | ||
}); | ||
} | ||
{{/enabled}} | ||
{{/js}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rohansharmasitoula
$api_base_url
itself can be used here