Skip to content
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

Merged
merged 19 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
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
80 changes: 63 additions & 17 deletions classes/local/api/tracker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Copy link
Contributor

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

$proctor_token = get_config('local_proview', 'token');
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rohansharmasitoula What is the benefit of using stream_context_create and file_get_contents instead of curl here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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, [
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);
}

}
}


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rohansharmasitoula Why are empty lines added at EOF

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed.





14 changes: 14 additions & 0 deletions fetch-secure-token.php
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']);
}
17 changes: 17 additions & 0 deletions store-fallback-details.php
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']);
}
153 changes: 110 additions & 43 deletions templates/tracker.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand All @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rohansharmasitoula This can be written as a function and reused

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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");
Expand All @@ -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");
Expand All @@ -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) {
Expand All @@ -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}}
4 changes: 2 additions & 2 deletions version.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@

defined('MOODLE_INTERNAL') || die;

$plugin->version = 2023091202;
$plugin->version = 2023092001;
$plugin->requires = 2020061500;
$plugin->release = '3.2.0 (Build: 2023091202)';
$plugin->release = '3.2.0 (Build: 2023092001)';
$plugin->maturity = MATURITY_STABLE;
$plugin->component = 'local_proview';

Expand Down
Loading