// ==UserScript==
// @name Quiz Response Userscript
// @namespace http://tampermonkey.net/
// @version 1.0
// @description userscript that adds answers creates dynamic html tags
// @author You
// @match https://academy.cs.cmu.edu/quiz/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=cmu.edu
// @grant none
// ==/UserScript==
function createHeaderandTitles(titleString, hasComplete){
const appDiv = document.getElementById("app");
const title = document.createElement("h1");
const isSubmitDiv = document.createElement("div");
const isSubmitText = document.createElement("p");
title.textContent = titleString;
isSubmitText.textContent = `Quiz Complete: ${hasComplete}`
isSubmitText.style.fontStyle = 'italic';
isSubmitDiv.appendChild(isSubmitText);
addLeftRightPadding(title);
title.style.paddingBottom = "15px";
appDiv.appendChild(title);
addLeftRightPadding(isSubmitDiv);
appDiv.appendChild(isSubmitDiv);
}
function addLeftRightPadding(element){
element.style.paddingLeft = '20px';
element.style.paddingRight = '20px';
}
function createTextElement(string, parentContainer=null){
let innerQuizContainer = document.getElementById("app");
const newDiv = document.createElement("div");
newDiv.classList.add("dynamic-text-usrc");
newDiv.innerHTML = string;
addLeftRightPadding(newDiv);
if (parentContainer){
try{
parentContainer.appendChild(newDiv);
innerQuizContainer.appendChild(parentContainer);
}catch(err){
console.error(err)
}
}else{
innerQuizContainer.appendChild(newDiv);
}
return newDiv;
}
/*
creates quiz stats.
@param {string} name string for quiz name
@param {number} quizID 4 digit num for id of quiz
@param {bool} answersReleased boolean value for avaliable answers
@param {number} timeSpent int for the amount of time in seconds spent away from quiz
@param {object} ctGrades object with grades, if not avaliable - will be empty {}
*/
function createQuizStats(name, quizID, answersReleased, timeSpent, ctGrades){
const containerDiv = document.createElement("div");
const quizName = document.createElement("h3");
quizName.textContent = "Quiz Stats";
addLeftRightPadding(containerDiv);
//append to container div
containerDiv.appendChild(quizName);
createTextElement(`ID: <b>${quizID}</b>`, containerDiv);
createTextElement(`Answers Released: <b>${answersReleased}</b>`, containerDiv);
createTextElement(`Time spent Away(seconds): <b>${timeSpent}</b>`, containerDiv);
createTextElement(`Released Grades: <b>${JSON.stringify(ctGrades)}</b>`, containerDiv);
}
function createExcerciseElement(title, imgUrl, points, metaId, score) {
const QuizContainer = document.getElementById("app");
const mainDiv = document.createElement("div");
mainDiv.classList.add("dynamic-exercise-div");
const titleElement = document.createElement("h2");
titleElement.textContent = title;
const mainUrl = document.createElement("a");
const imageContainer = document.createElement("div");
const pointText = document.createElement("p");
pointText.textContent = `Points: ${points}`;
const totalScoredText = document.createElement("p");
totalScoredText.textContent = `Scored: ${score}`
totalScoredText.style.fontStyle = "italic";
imageContainer.classList.add("image-container");
//create label and checkbox form elem
const labelCheckbox = document.createElement("label");
labelCheckbox.for = "iframe-box";
labelCheckbox.textContent = "Enable/Disable Iframe";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.setAttribute("id", "iframe-box");
labelCheckbox.style.marginRight = "5px";
//now add change event listener
const iframeElement = document.createElement("iframe");
checkbox.addEventListener( "change", () => {
if ( checkbox.checked ) {
//enable iframe
iframeElement.src = `https://academy.cs.cmu.edu/exercise/${metaId}/`;
iframeElement.height = "700";
iframeElement.width = "100%";
mainDiv.appendChild(iframeElement);
} else {
//pass
iframeElement.remove()
}
});
mainUrl.href = `https://academy.cs.cmu.edu/exercise/${metaId}/`;
//gets image as blob
fetch(`https://academy.cs.cmu.edu${imgUrl}`)
.then((response) => response.blob())
.then((blob) => {
const imageUrl = URL.createObjectURL(blob);
const imageElement = document.createElement("img");
imageElement.src = imageUrl;
mainUrl.appendChild(imageElement);
imageContainer.appendChild(mainUrl);
//append all
//add points attribute
mainDiv.setAttribute("points", points);
mainDiv.setAttribute("scored", score);
mainDiv.appendChild(titleElement);
mainDiv.appendChild(labelCheckbox);
mainDiv.appendChild(checkbox);
mainDiv.appendChild(pointText);
mainDiv.appendChild(totalScoredText);
mainDiv.appendChild(mainUrl);
addLeftRightPadding(mainDiv);
QuizContainer.appendChild(mainDiv);
})
.catch((err) => {
console.error("fetch error", err);
});
}
function keepAnswersOnlyArray(array){
return array.filter((element) => {return element.className === "dynamic-answer-elem"});
}
function makeFetchRequest(url){
const authToken = localStorage["cs-academy-token"];
fetch(url, { "headers": {"Authorization": `Token ${authToken}`} })
.then((response) => {
return response.json()
})
.then((response) => {
console.debug(response); //log response(dev)
const content = response.content;
let answersArray = []
let totalPointsArray = []
//intalize, create correct header and titles
createHeaderandTitles(response.name, response.quizComplete);
// correct ans button add
correctAnswersButton();
//go over response content
content.forEach((item) => {
//keeping as if in case multiple pop up in same use?
//can change to else if for perf
if (item.toc_entry == "True or False"){
createTextElement("<b>" + item.chunks[0].content + "</b>");
}
if (item.type == "writeup" && item.toc_entry == "Multiple Choice"){
const element = createTextElement(item.chunks[0].content);
element.style.fontWeight = "900";
element.style.paddingTop = "20px";
}
if (item.type == "mc"){
createTextElement(item.question.text);
//item answers
const parentAnswerDiv = document.createElement("div");
parentAnswerDiv.classList.add("dynamic-answer-div");
parentAnswerDiv.style.padding = "5px"
//add points to points array
totalPointsArray.push(item.points);
//loop over answers for elements.
item.answers.forEach((item) => {
const element = createTextElement(item.text, parentAnswerDiv);
element.style.paddingTop = "5px";
element.style.paddingBottom = "5px";
element.classList.remove("dynamic-text-usrc");
element.classList.add("dynamic-answer-elem");
answersArray.push(element);
});
}
if (item.type == "exercise"){
createExcerciseElement(item.title, item.icon_url, item.points, item.meta_id, response.exerciseAnswers[0].score);
}
});
//create quiz stats
createQuizStats(`${response.number} ${response.name}`, response.id, response.showAnswers, response.awaySeconds, response.ctGrades);
const matchedArray = Object.values(response.mcAnswers);
const mcArray = document.querySelectorAll(".dynamic-answer-div");
console.debug(response.mcAnswers, mcArray);
console.debug(matchedArray);
console.debug("points", totalPointsArray);
totalPointsArray.forEach((number, index) => {
//add points to all divs
mcArray[index].setAttribute("points", number);
const pointsText = createTextElement(`<b>Points:</b> ${number}`)
pointsText.style.margin = "10px";
mcArray[index].prepend(pointsText);
});
//match answers
matchedArray.forEach((item, index) => {
let answerArray = Array.from(mcArray[index].children);
answerArray = keepAnswersOnlyArray(answerArray);
const answerElement = answerArray[item];
answerElement.style.backgroundColor = "#FFFF33";
//for easier html parsing boy - usually can set to "" but i make it true for the fun of it ig
// note it's just a check jamal - so it's not on all..
answerElement.setAttribute("isanswer", true);
});
})
.catch((err) => {
console.error(err);
});
}
const quizID = window.location.href.replace("https://academy.cs.cmu.edu/quiz/", "");
if (quizID.length > 0){
try{
makeFetchRequest(`https://backend.academy.cs.cmu.edu/api/v0/quiz/?id=${quizID}`);
}catch(err){
console.error("An error occurred during the fetch request. Is it a valid quizID? Trace below.", err);
}
}
//button bullshite for checking answers
let savedBackground = null;
//handles click - changes background to green
function handleAnswerClick(event){
const parent = event.target.parentElement;
//checks for clicked attr, if not true then go ahead, so user only clicks once
if (event.target.className == "dynamic-answer-elem"){
if (!parent.hasAttribute("hasClicked")){
parent.setAttribute("hasClicked", true);
//save the previous bg
savedBackground = event.target.style.backgroundColor;
event.target.style.backgroundColor = "#AFE1AF";
event.target.setAttribute("userAnswer", true);
}else if (event.target.hasAttribute("userAnswer")){
event.target.style.backgroundColor = `${savedBackground}`;
event.target.removeAttribute("userAnswer");
parent.removeAttribute("hasClicked");
}
}
}
//checks how many elements have the answers attr
function checkAnswersBackground(button){
//check all answer div elements to see if the user clicked on them atleast once
const divElements = Array.from(document.querySelectorAll(".dynamic-answer-div"));
//bool for is reliable meaning user marked down all possible ans
const isReliable = divElements.every(element => element.hasAttribute("hasclicked"))
if (!isReliable){
if (!button.parentElement.querySelector("p")){
//bring up a text over the button that lets user know it's not all selected.
const text = document.createElement("p");
text.textContent = "⚠️ WARNING: The points result may not be accurate as you have not selected all elements."
text.style.color = "#cc3300";
text.style.padding = "5px";
button.parentElement.appendChild(text);
}
}else{
const parentSelectorP = button.parentElement.querySelector("p");
if (parentSelectorP){
parentSelectorP.remove();
}
}
const elements = document.querySelectorAll(".dynamic-answer-div");
const exercises = document.querySelectorAll(".dynamic-exercise-div");
let maxPoints = 0;
//use a closure to parse the ints
function parsePointsInt(value){
try{
const points = parseInt(value, 10);
// Check if is not num
if (!isNaN(points)) {
maxPoints += points; // add parsed num to num
} else {
console.error("Invalid points value:", value);
}
}catch(err){
console.error("Is the attribute correct? Error:", err);
}
}
let answerElements = []
elements.forEach((elem) => {
parsePointsInt(elem.attributes.points.value);
answerElements.push(keepAnswersOnlyArray(Array.from(elem.children)));
});
//doing this for later expansion? maybe..?
const userAnswerObjects = []
const webAnswerObjects = []
answerElements.forEach((elem, index) => {
//gives if answer is selected from the answers content
answerElements[index].forEach((item) => {
const webAnswer = {
element: item,
answerBoolean: item.hasAttribute("isanswer"),
points: item.parentElement.attributes.points.value,
type: "webAnswer"
}
const Userans = {
element: item,
answerBoolean: item.hasAttribute("useranswer"),
points: item.parentElement.attributes.points.value,
type: "userAnswer"
}
webAnswerObjects.push(webAnswer);
userAnswerObjects.push(Userans);
});
});
//init the var for how many points userscored
let pointsScored = 0;
//using a for loop means the 2 arrays must be the same length
for (let i = 0; i < userAnswerObjects.length; i++) {
const userAnswer = userAnswerObjects[i];
const webAnswer = webAnswerObjects[i];
if (userAnswer.answerBoolean && webAnswer.answerBoolean) {
//finds matches and addes the correct num of points
pointsScored += (parseInt(userAnswer.points || webAnswer.points)) ?? new Error("Something went wrong during points processing.");
}
}
console.debug(userAnswerObjects);
console.debug(webAnswerObjects);
exercises.forEach((elem) => {
parsePointsInt(elem.attributes.points.value);
pointsScored += parseInt(elem.attributes.scored.value);
});
//log score
console.debug("Score:", pointsScored, maxPoints);
function createResultText(score, maxNum){
const resultText = document.createElement("p");
resultText.textContent = `Test Result: ${score}/${maxNum}`
resultText.style.margin = "5px"
resultText.classList.add("dynamic-result-text");
button.parentElement.appendChild(resultText);
}
//see elements
const resultTextElement = document.querySelector(".dynamic-result-text");
if (!resultTextElement){
createResultText(pointsScored, maxPoints);
}else{
resultTextElement.remove();
createResultText(pointsScored, maxPoints);
}
}
//add correct answers!
function correctAnswersButton(){
console.debug("Add answers button.");
const containerDiv = document.createElement("div");
const mainButton = document.createElement("button");
mainButton.style.background = "#04AA6D";
mainButton.style.border = "none";
mainButton.style.borderRadius = "4px";
mainButton.style.width = "120px";
mainButton.style.color = "white";
mainButton.style.height = "35px";
mainButton.textContent = "Add Answers";
mainButton.classList.add("answer-button");
addLeftRightPadding(containerDiv);
containerDiv.style.paddingBottom = "15px";
const appElement = document.getElementById("app");
let hasSelected = false;
mainButton.addEventListener("click", () => {
if (!hasSelected){
mainButton.textContent = "Stop Select"
mainButton.style.background = "#913831";
document.addEventListener("click", handleAnswerClick);
hasSelected = true;
}else{
mainButton.style.background = "#04AA6D";
mainButton.textContent = "Add Answers";
document.removeEventListener("click", handleAnswerClick);
checkAnswersBackground(mainButton);
hasSelected = false;
}
});
containerDiv.appendChild(mainButton);
appElement.appendChild(containerDiv);
return mainButton;
}