├── favicon.png ├── manifest.json ├── README.md ├── popup.html └── popup.js /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fahadsheikh003/Jugaadu-Flex-2/HEAD/favicon.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Jugaadu Flex", 3 | "description": "A Chrome Extension to make FlexStudent better", 4 | "manifest_version": 3, 5 | "version": "2.2.1", 6 | "permissions": ["scripting", "tabs"], 7 | "host_permissions": ["*://flexstudent.nu.edu.pk/*"], 8 | "action": { 9 | "default_popup": "popup.html" 10 | }, 11 | "icons": { 12 | "128": "favicon.png" 13 | } 14 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jugaadu-Flex-2.0 2 | 3 | This is a Chrome extension that allows you to view your Grand Total Marks (client side calculation), Select feedback with a single clock in flex student portal easily and calculates SGPA (current semester) and updates CGPA. Contributions and recommendations are welcome. 4 | 5 |

6 | 7 |

8 | 9 | ## Installation Procedure 10 | Follow this installation procedure to get started with the extension. The installation is dependency free and only requires Chrome. 11 | 12 | * Clone this repository. 13 | * Unzip the folder. 14 | * Visit ```chrome://extensions/```. 15 | * Enable developer mode. 16 | * Click on ```Load unpacked``` and select the unzipped folder. 17 | 18 | ## Usage 19 | ### Marks 20 | * Visit Marks page on FlexStudent Portal i.e., ```flexstudent.nu.edu.pk/Student/StudentMarks```. 21 | * Click on extension icon and Click on ```Fix Grand Total Marks``` button. 22 | * And Boom! all your marks are back where they should be. 23 | #### Note 24 | * Only Total Marks calculated are precise, the rest (average, minimum, and maximum) are just calculated based on statistics available on the page because we don't have data of all students that's why we can't calculate class average, minimum and maximum marks. 25 | 26 | ### Feedback 27 | * Visit Feedback page of a course on FlexStudent Portal i.e., ```flexstudent.nu.edu.pk/Student/FeedBackQuestions```. 28 | * Click on extension icon and Select your desired Feedback option. 29 | * Click on ```Select``` button. 30 | * And Boom! you will see the magic by yourself. 31 | 32 | ### Admit Card 33 | * Select Exam term and click on Get Admit card. 34 | 35 | ### GPA Calculator 36 | * Visit Transcript page on FlexStudent Portal i.e., ```flexstudent.nu.edu.pk/Student/Transcript```. 37 | * Click on extension icon and Click on ```Turn on GPA Calculator``` button. 38 | * Now, just select your estimated grades for each of the courses. 39 | * And Boom! you will start seeing changes as soon as you start updating your grades. 40 | 41 |

42 | 43 |

44 | 45 | ## Disclaimer 46 | This code is provided for educational purposes only. The code is not intended to be used for malicious purposes or to gain unauthorized access to any system. The author of this code takes no responsibility for any misuse of the code. Use at your own risk. 47 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 |
13 |

Jugaadu Flex 2.0

14 |
15 | 16 |
17 | 18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 |
27 | 28 |
29 | 34 | 35 |
36 | 37 |
38 | 39 |
40 |

Feedback

41 |
42 | 46 | 50 | 54 |
55 |
56 | 60 | 64 | 68 |
69 | 70 | 71 |
72 | 73 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | const marksForm = document.getElementById("grand-marks"); 2 | marksForm.addEventListener("submit", handleMarksFormSubmit); 3 | 4 | const feedbackForm = document.getElementById("feedback-form"); 5 | feedbackForm.addEventListener("submit", handleFeedbackFormSubmit); 6 | 7 | const gpaCalculatorForm = document.getElementById("gpa-calculator"); 8 | gpaCalculatorForm.addEventListener("submit", handleCalculatorFormSubmit); 9 | 10 | const admitCardForm = document.getElementById("admit-card"); 11 | admitCardForm.addEventListener("submit", handleAdmitCardSubmit); 12 | 13 | async function handleMarksFormSubmit(event) { 14 | event.preventDefault(); 15 | let [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 16 | let url; 17 | if (tab?.url) { 18 | try { 19 | url = new URL(tab.url); 20 | if (url.hostname !== "flexstudent.nu.edu.pk") { 21 | alert("Please open the FlexStudent website first."); 22 | return; 23 | } 24 | } catch {} 25 | } 26 | 27 | chrome.scripting.executeScript({ target: { tabId: tab.id }, function: marksMainFunction }); 28 | } 29 | 30 | async function handleCalculatorFormSubmit(event) { 31 | event.preventDefault(); 32 | let [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 33 | let url; 34 | if (tab?.url) { 35 | try { 36 | url = new URL(tab.url); 37 | if (url.hostname !== "flexstudent.nu.edu.pk") { 38 | alert("Please open the FlexStudent website first."); 39 | return; 40 | } 41 | } catch {} 42 | } 43 | 44 | chrome.scripting.executeScript({ target: { tabId: tab.id }, function: calculatorMainFunction }); 45 | } 46 | 47 | async function handleFeedbackFormSubmit(event) { 48 | event.preventDefault(); 49 | let [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 50 | let url; 51 | if (tab?.url) { 52 | try { 53 | url = new URL(tab.url); 54 | if (url.hostname !== "flexstudent.nu.edu.pk") { 55 | alert("Please open the FlexStudent website first."); 56 | return; 57 | } 58 | } catch {} 59 | } 60 | 61 | const input = document.querySelector('input[name="feedback-radio"]:checked'); 62 | if (!input) { 63 | alert("Please select a feedback option first."); 64 | return; 65 | } 66 | 67 | chrome.scripting.executeScript({ target: { tabId: tab.id }, function: feedbackMainFunction, args: [input.value] }); 68 | } 69 | 70 | async function marksMainFunction() { 71 | if (!window.location.href.includes("Student/StudentMarks")) { 72 | alert("Please Open Marks Page First"); 73 | return; 74 | } 75 | 76 | const getTd = (className, id) => { 77 | const td = document.createElement('td'); 78 | td.classList.add("text-center"); 79 | td.classList.add(className); 80 | td.id = id; 81 | return td; 82 | } 83 | 84 | const getTr = (id) => { 85 | const tr = document.createElement('tr'); 86 | tr.classList.add("totalColumn_" + id); 87 | tr.appendChild(getTd("totalColGrandTotal", "GrandtotalColMarks_" + id)); 88 | tr.appendChild(getTd("totalColObtMarks", "GrandtotalObtMarks_" + id)); 89 | tr.appendChild(getTd("totalColAverageMark", "GrandtotalClassAvg_" + id)); 90 | tr.appendChild(getTd("totalColMinMarks", "GrandtotalClassMin_" + id)); 91 | tr.appendChild(getTd("totalColMaxMarks", "GrandtotalClassMax_" + id)); 92 | tr.appendChild(getTd("totalColStdDev", "GrandtotalClassStdDev_" + id)); 93 | return tr; 94 | } 95 | 96 | const parseFloatOrZero = (value) => { 97 | const parsedValue = parseFloat(value); 98 | return isNaN(parsedValue) ? 0 : parsedValue; 99 | } 100 | 101 | const checkBestOff = (section, weightage) => { 102 | const calculationRows = section.querySelectorAll(`.calculationrow`); 103 | let weightsOfAssessments = 0; 104 | let count = 0; 105 | for (let row of calculationRows) { 106 | const weightageOfAssessment = parseFloatOrZero(row.querySelector('.weightage').textContent); 107 | weightsOfAssessments += weightageOfAssessment; 108 | 109 | if (weightage < weightsOfAssessments) { 110 | return count; 111 | } 112 | count++; 113 | } 114 | return count; 115 | } 116 | 117 | const reorderCalculationRows = (section, bestOff) => { 118 | const sectionArray = Array.from(section.querySelectorAll(`.calculationrow`)); 119 | sectionArray.sort((a, b) => { 120 | const aObtained = parseFloatOrZero(a.querySelector('.ObtMarks').textContent); 121 | const bObtained = parseFloatOrZero(b.querySelector('.ObtMarks').textContent); 122 | return bObtained - aObtained; 123 | }); 124 | return sectionArray.slice(0, bestOff); 125 | } 126 | 127 | async function set_marks(courseId, id) { 128 | const course = document.getElementById(courseId); 129 | const sections = course.querySelectorAll(`div[id^="${courseId}"]:not([id$="Grand_Total_Marks"])`); 130 | 131 | let globalWeightage = 0; 132 | let globalObtained = 0; 133 | let globalAverage = 0; 134 | let globalMinimum = 0; 135 | let globalMaximum = 0; 136 | 137 | for (let section of sections) { 138 | const totalRow = section.querySelector(`.totalColumn_${id}`); 139 | const localWeightage = parseFloat(totalRow.querySelector('.totalColweightage').textContent); 140 | const localObtained = parseFloat(totalRow.querySelector('.totalColObtMarks').textContent); 141 | 142 | globalWeightage += localWeightage; 143 | globalObtained += localObtained; 144 | 145 | // Check if there are any best off marks 146 | const bestOff = checkBestOff(section, localWeightage); 147 | const calculationRows = reorderCalculationRows(section, bestOff); 148 | 149 | for (let row of calculationRows) { 150 | const weightage = parseFloatOrZero(row.querySelector('.weightage').textContent); 151 | const obtained = parseFloatOrZero(row.querySelector('.ObtMarks').textContent); 152 | const total = parseFloatOrZero(row.querySelector('.GrandTotal').textContent); 153 | const average = parseFloatOrZero(row.querySelector('.AverageMarks').textContent); 154 | const minimum = parseFloatOrZero(row.querySelector('.MinMarks').textContent); 155 | const maximum = parseFloatOrZero(row.querySelector('.MaxMarks').textContent); 156 | 157 | globalAverage += average * (weightage / total); 158 | globalMinimum += minimum * (weightage / total); 159 | globalMaximum += maximum * (weightage / total); 160 | } 161 | } 162 | 163 | document.getElementById(`GrandtotalColMarks_${id}`).textContent = globalWeightage.toFixed(2); 164 | document.getElementById(`GrandtotalObtMarks_${id}`).textContent = globalObtained.toFixed(2); 165 | document.getElementById(`GrandtotalClassAvg_${id}`).textContent = globalAverage.toFixed(2); 166 | document.getElementById(`GrandtotalClassMin_${id}`).textContent = globalMinimum.toFixed(2); 167 | document.getElementById(`GrandtotalClassMax_${id}`).textContent = globalMaximum.toFixed(2); 168 | } 169 | 170 | const courses = document.querySelectorAll(`div[class*='tab-pane']`); // Get all courses 171 | 172 | for (let i = 0; i < courses.length; i++) { 173 | const courseId = courses[i].id; 174 | const button = courses[i].querySelector(`button[onclick*="ftn_calculateMarks"]`); 175 | if (button) { 176 | const id = parseInt(button.getAttribute('onclick').substring(20, 24)); 177 | const newTr = getTr(id); 178 | courses[i].querySelector(`div[id=${courses[i].id}-Grand_Total_Marks]`).querySelector('tbody').innerHTML = ''; 179 | courses[i].querySelector(`div[id=${courses[i].id}-Grand_Total_Marks]`).querySelector('tbody').appendChild(newTr); 180 | set_marks(courseId, id); 181 | } 182 | } 183 | } 184 | 185 | async function feedbackMainFunction(input) { 186 | if (!window.location.href.includes("Student/FeedBackQuestions")) { 187 | alert("Please Open Feedback Page of a Specific Course First"); 188 | return; 189 | } 190 | 191 | function selectSpecificRadio(element, input) { 192 | const radioButtonsSpan = element.getElementsByClassName('m-list-timeline__time'); 193 | for (let i = 0; i < radioButtonsSpan.length; i++) { 194 | if (radioButtonsSpan[i].textContent.trim() === input) { 195 | const radioButton = radioButtonsSpan[i].querySelector('input[type="radio"]'); 196 | radioButton.checked = true; 197 | break; 198 | } 199 | } 200 | } 201 | 202 | function selectSpecificFeedback(input) { 203 | const questions = document.getElementsByClassName('m-list-timeline__item'); 204 | Array.from(questions).forEach(question => { 205 | selectSpecificRadio(question, input); 206 | }); 207 | } 208 | 209 | function selectRandomFeedback() { 210 | const questions = document.getElementsByClassName('m-list-timeline__item'); 211 | Array.from(questions).forEach(question => { 212 | const radioButtonsSpan = question.getElementsByClassName('m-list-timeline__time'); 213 | const randomIndex = Math.floor(Math.random() * radioButtonsSpan.length); 214 | const radioButton = radioButtonsSpan[randomIndex].querySelector('input[type="radio"]'); 215 | radioButton.checked = true; 216 | }); 217 | } 218 | 219 | input === "Randomize" ? selectRandomFeedback() : selectSpecificFeedback(input); 220 | } 221 | 222 | async function calculatorMainFunction() { 223 | if (!window.location.href.includes("Student/Transcript")) { 224 | alert("Please Open Transcript Page first"); 225 | return; 226 | } 227 | 228 | const getSelect = (currGrade) => { 229 | return ``; 245 | } 246 | 247 | const getSUcredithours = () => { 248 | return Array.from(document.getElementsByTagName('td')) 249 | .filter((td) => td.innerText == 'S' || td.innerText == 'U') 250 | .reduce((total, curr) => total + parseInt(curr.previousElementSibling.innerText), 0); 251 | } 252 | 253 | let semesters = document.getElementsByClassName("col-md-6"); 254 | let lastSemester = semesters[semesters.length - 1]; 255 | let spans = lastSemester.querySelectorAll("span"); 256 | 257 | let cgpa = 0; 258 | let cgpaelem = spans[2]; 259 | let sgpaelem = spans[3]; 260 | 261 | let crEarned = 0; 262 | 263 | if(semesters.length > 1){ 264 | let secondLastSemester = semesters[semesters.length - 2]; 265 | crEarned = parseInt(secondLastSemester.querySelectorAll("span")[1].innerText.split(':')[1]); 266 | cgpa = parseFloat(secondLastSemester.querySelectorAll("span")[2].innerText.split(':')[1]); 267 | } 268 | 269 | let rows = lastSemester.querySelectorAll('tbody > tr'); 270 | 271 | for (let row of rows) { 272 | const gradeCell = row.querySelectorAll('td.text-center')[1]; 273 | const currentGradeText = gradeCell.innerText.trim(); 274 | gradeCell.innerHTML = getSelect(currentGradeText); 275 | } 276 | 277 | const getCorrespondingCreditHours = (selectelem) => parseInt(selectelem.parentElement.previousElementSibling.innerText); 278 | 279 | const handleSelectChange = (e) => { 280 | let selects = document.getElementsByTagName("select"); 281 | let totalCreditHours = 0; 282 | let totalGradePoints = 0; 283 | 284 | // Collect all courses from all semesters 285 | const allCourses = []; 286 | for (let i = 0; i < semesters.length; i++) { 287 | let semesterRows = semesters[i].querySelectorAll("tbody > tr"); 288 | for (let row of semesterRows) { 289 | const courseName = row.querySelector("td:nth-child(2)").innerText.trim(); 290 | const creditHours = parseInt(row.querySelector("td:nth-child(4)").innerText); 291 | const gradeCell = row.querySelector("td:nth-child(5)"); 292 | let gradeValue = -1; 293 | 294 | // Check if the grade cell contains a select element (for current semester) 295 | if (gradeCell.querySelector("select")) { 296 | gradeValue = parseFloat(gradeCell.querySelector("select").value); 297 | } else { 298 | // For previous semesters, parse the existing grade text 299 | const gradeText = gradeCell.innerText.trim(); 300 | switch (gradeText) { 301 | case "A+": 302 | case "A": 303 | gradeValue = 4; 304 | break; 305 | case "A-": 306 | gradeValue = 3.67; 307 | break; 308 | case "B+": 309 | gradeValue = 3.33; 310 | break; 311 | case "B": 312 | gradeValue = 3; 313 | break; 314 | case "B-": 315 | gradeValue = 2.67; 316 | break; 317 | case "C+": 318 | gradeValue = 2.33; 319 | break; 320 | case "C": 321 | gradeValue = 2; 322 | break; 323 | case "C-": 324 | gradeValue = 1.67; 325 | break; 326 | case "D+": 327 | gradeValue = 1.33; 328 | break; 329 | case "D": 330 | gradeValue = 1; 331 | break; 332 | case "F": 333 | gradeValue = 0; 334 | break; 335 | case "S": 336 | gradeValue = -2; 337 | break; 338 | case "U": 339 | gradeValue = -3; 340 | break; 341 | default: 342 | gradeValue = -1; // Handle cases where grade is not a standard letter grade 343 | } 344 | } 345 | 346 | if (gradeValue !== -1 && gradeValue !== -2 && gradeValue !== -3) { 347 | allCourses.push({ name: courseName, creditHours: creditHours, gradeValue: gradeValue }); 348 | } 349 | } 350 | } 351 | 352 | // Determine the latest grade for each course (overwrite as we go) 353 | const courseLatestGrades = {}; 354 | for (const course of allCourses) { 355 | courseLatestGrades[course.name] = course; 356 | } 357 | 358 | // Calculate total credit hours and grade points based on latest grades only 359 | let finalTotalCreditHours = 0; 360 | let finalTotalGradePoints = 0; 361 | for (const courseName in courseLatestGrades) { 362 | const course = courseLatestGrades[courseName]; 363 | finalTotalCreditHours += course.creditHours; 364 | finalTotalGradePoints += course.creditHours * course.gradeValue; 365 | } 366 | 367 | // Update the displayed grades for the current semester 368 | for (let select of selects) { 369 | if (select.value != -1 && select.value != -2 && select.value != -3) { 370 | select.parentElement.nextElementSibling.innerText = select.value; 371 | select.parentElement.nextElementSibling.style.fontWeight = 'bold'; 372 | } else if (select.value == -2) { 373 | select.parentElement.nextElementSibling.innerText = 'S'; 374 | select.parentElement.nextElementSibling.style.fontWeight = 'normal'; 375 | } else if (select.value == -3) { 376 | select.parentElement.nextElementSibling.innerText = 'U'; 377 | select.parentElement.nextElementSibling.style.fontWeight = 'normal'; 378 | } else { 379 | select.parentElement.nextElementSibling.innerText = '-'; 380 | select.parentElement.nextElementSibling.style.fontWeight = 'normal'; 381 | } 382 | } 383 | 384 | if (finalTotalCreditHours === 0) { 385 | cgpaelem.innerHTML = `CGPA: ${cgpa.toFixed(2)}`; 386 | sgpaelem.innerHTML = `SGPA: 0`; 387 | return; 388 | } 389 | 390 | // Calculate SGPA for current semester only 391 | for (let select of selects) { 392 | if (select.value != -1 && select.value != -2 && select.value != -3) { 393 | totalCreditHours += getCorrespondingCreditHours(select); 394 | totalGradePoints += parseFloat(getCorrespondingCreditHours(select)) * parseFloat(select.value); 395 | } 396 | } 397 | 398 | const calculatedSGPA = totalCreditHours > 0 ? totalGradePoints / totalCreditHours : 0; 399 | const calculatedCGPA = finalTotalGradePoints / finalTotalCreditHours; // CGPA is based on highest grades across all semesters 400 | 401 | cgpaelem.innerHTML = `CGPA: ${calculatedCGPA.toFixed(2)}`; 402 | sgpaelem.innerHTML = `SGPA: ${calculatedSGPA.toFixed(2)}`; 403 | 404 | // set cgpaelem and sgpaelem to bold 405 | cgpaelem.style.fontWeight = 'bold'; 406 | sgpaelem.style.fontWeight = 'bold'; 407 | } 408 | 409 | // add event listener to all select elements 410 | Array.from(document.getElementsByTagName('select')).forEach((select) => { 411 | select.addEventListener('change', handleSelectChange) 412 | }); 413 | 414 | handleSelectChange(); 415 | } 416 | 417 | async function handleAdmitCardSubmit(event) { 418 | let [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 419 | let url; 420 | if (tab?.url) { 421 | try { 422 | url = new URL(tab.url); 423 | if (url.hostname !== "flexstudent.nu.edu.pk") { 424 | alert("Please open the FlexStudent website first."); 425 | return; 426 | } 427 | } catch {} 428 | } 429 | 430 | const input = document.getElementById("admit-card-radio"); 431 | if (!input) { 432 | alert("Please select an option first."); 433 | return; 434 | } 435 | 436 | chrome.scripting.executeScript({ target: { tabId: tab.id }, function: admitCardMainFunction, args: [input.value] }); 437 | } 438 | 439 | async function admitCardMainFunction(inputValue) { 440 | if (!(inputValue === "Sessional-I" || inputValue === "Sessional-II" || inputValue === "Final")) { 441 | return; 442 | } 443 | const resp = await fetch(`https://flexstudent.nu.edu.pk/Student/AdmitCardByRollNo?cardtype=${inputValue}&type=pdf`, { 444 | method: 'POST' 445 | }); 446 | const blob = await resp.blob(); 447 | const url = URL.createObjectURL(blob); 448 | 449 | let a = document.createElement('a'); 450 | a.href = url; 451 | a.download = `Admit_Card_${inputValue}.pdf`; 452 | a.click(); 453 | URL.revokeObjectURL(url); 454 | } --------------------------------------------------------------------------------