diff --git a/public/js/buttonFunctions.js b/public/js/buttonFunctions.js index 48eaa900..f8544406 100755 --- a/public/js/buttonFunctions.js +++ b/public/js/buttonFunctions.js @@ -367,6 +367,12 @@ let exportTableData = async function(prefs) { }), filename); }; +/** + * Imports a JSON file and adds the data as a TableDataObject to the + * + * @param {object} obj - Data loaded from json file + * @returns {Promise} + */ let importTableData = async function(obj) { //#ifndef lite diff --git a/public/js/calculate_grade.js b/public/js/calculate_grade.js index 42cf49c7..a49cf486 100755 --- a/public/js/calculate_grade.js +++ b/public/js/calculate_grade.js @@ -1,3 +1,13 @@ +/** + * + * @param {Object[]} assignments + * @param {Object} categories + * @param decimals + * @param init_grade + * @param {string} grade + * @returns {string|{categoryScores: {}, categoryPercent: string, categoryGrades: {}, totalPercent: string, categoryMaxScores: {}}} + */ + function computeGrade(assignments, categories, decimals, init_grade, grade) { let categoryScores = {}, categoryMaxScores = {}, categoryGrades = {}; for (let category in categories) { @@ -8,13 +18,13 @@ function computeGrade(assignments, categories, decimals, init_grade, grade) { let totalScore = 0, totalMaxScore = 0; if (Object.keys(categories).length === 0) { - for (let j = 0; j < assignments.length; j++) { - totalScore += parseFloat(assignments[j].score); - totalMaxScore += parseFloat(assignments[j].max_score); + for (let assignment of assignments) { + totalScore += parseFloat(assignment.score); + totalMaxScore += parseFloat(assignment.max_score); } let totalPercent = totalScore / totalMaxScore; - return "" + (Math.round(totalPercent * 10000) / 100); + return (Math.round(totalPercent * 10000) / 100).toString(); } else { for (let i = 0; i < assignments.length; i++) { if (!isNaN(assignments[i].score)) { @@ -34,18 +44,18 @@ function computeGrade(assignments, categories, decimals, init_grade, grade) { categoryGrades[category] = "N/A"; } else { categoryGrades[category] = (0.0 + categoryScores[category]) / categoryMaxScores[category]; - categoryPercent += ((0.0 + categoryScores[category]) / categoryMaxScores[category]) * parseFloat(categories[category]); + categoryPercent += (0.0 + categoryScores[category]) / categoryMaxScores[category] * parseFloat(categories[category]); } } categoryPercent /= counterWeight; let totalPercent = totalScore / totalMaxScore; - let output = (parseFloat(grade)/100) + categoryPercent - parseFloat(init_grade); + let output = parseFloat(grade)/100 + categoryPercent - parseFloat(init_grade); return { - categoryPercent: "" + (Math.round(output * Math.pow(10, decimals + 2)) / Math.pow(10, decimals)), - totalPercent: "" + (Math.round(totalPercent * Math.pow(10, decimals + 2)) / Math.pow(10, decimals)), + categoryPercent: (Math.round(output * Math.pow(10, decimals + 2)) / Math.pow(10, decimals)).toString(), + totalPercent: (Math.round(totalPercent * Math.pow(10, decimals + 2)) / Math.pow(10, decimals)).toString(), categoryScores, categoryMaxScores, categoryGrades, @@ -53,6 +63,13 @@ function computeGrade(assignments, categories, decimals, init_grade, grade) { } } +/** + * + * @param assignments + * @param categories + * @param currentGrade + * @returns {string|{categoryScores: {}, categoryGrades: {}, categoryPercent: number, type: string, categoryMaxScores: {}}} + */ function determineGradeType(assignments, categories, currentGrade) { let categoryScores = {}, categoryMaxScores = {}, categoryGrades = {}; for (let category in categories) { diff --git a/public/js/home.js b/public/js/home.js index a6e19391..6ac843b3 100644 --- a/public/js/home.js +++ b/public/js/home.js @@ -1,3 +1,5 @@ +// State + const termConverter = ['current', 'q1', 'q2', 'q3', 'q4']; let pdf_index = 0; let pdfrendering = false; @@ -13,8 +15,23 @@ let exportModal = document.getElementById('export_modal'); let importModal = document.getElementById('import_modal'); let term_dropdown_active = true; let currentTerm = "current"; +/** + * All the data provided by Aspine API + * @typedef TableDataObject + * @property {GPA} [cumGPA] + * @property {Term} [currentTermData] + * @property {string} [name] + * @property {Overview[]} [overview] + * @property {RecentActivity} [recent] + * @property {Schedule} [schedule] + * @property {Terms} [terms] + * @property {?string} [username] + * @property {boolean} [imported=true] + */ +/** @type {TableDataObject[]}*/ let tableData = [{ name: "Current Year" }]; let currentTableDataIndex = 0; +/**@type {TableDataObject}*/ let currentTableData = tableData[currentTableDataIndex]; let selected_class_i; let termsReset = {}; @@ -79,15 +96,29 @@ let noStats = function() { document.getElementById("stats_modal_content").style.height = "5rem"; }; +/** + * Hide a modal window + * @param {string} key Name of modal window. + */ let hideModal = function(key) { modals[key].style.display = "none"; - if (key === "stats") noStats(); - - if (key === "corrections") { - document.getElementById("corrections_modal_input").value = ""; + switch (key) + { + case 'stats': + noStats(); + break; + case 'corrections': + document.getElementById("corrections_modal_input").value = ""; + break; + default: + console.error(`${key} is not a valid modal name`) } } +/** + * Un-hide a modal window + * @param {string} key Name of modal window. + */ let showModal = function(key) { modals[key].style.display = "inline-block"; } @@ -105,14 +136,14 @@ let recentAttendance = new Tabulator("#recentAttendance", { let recentActivity = new Tabulator("#recentActivity", { // height: 400, - layout:"fitColumns", + layout: "fitColumns", columns: [ - {title:"Date", field:"date", formatter: rowFormatter}, - {title:"Class", field:"classname", formatter: classFormatter}, - {title:"Assignment", field:"assignment", formatter: rowFormatter, headerSort: false}, - {title:"Score", field:"score", formatter: rowFormatter, headerSort: false}, - {title:"Max Score", field:"max_score", formatter: rowFormatter, headerSort: false}, - {title:"Percentage", field:"percentage", formatter: rowGradeFormatter}, + {title: "Date", field: "date", formatter: rowFormatter}, + {title: "Class", field: "classname", formatter: classFormatter}, + {title: "Assignment", field: "assignment", formatter: rowFormatter, headerSort: false}, + {title: "Score", field: "score", formatter: rowFormatter, headerSort: false}, + {title: "Max Score", field: "max_score", formatter: rowFormatter, headerSort: false}, + {title: "Percentage", field: "percentage", formatter: rowGradeFormatter}, ], rowClick: function(e, row) { //trigger an alert message when the row is clicked // questionable @@ -160,11 +191,11 @@ let categoriesTable = new Tabulator("#categoriesTable", { layout: "fitColumns", layoutColumnsOnNewData: true, columns: [ - {title:"Category", field:"category", formatter: rowFormatter, headerSort: false}, - {title:"Weight", field:"weight", formatter:weightFormatter, headerSort: false}, - {title:"Score", field:"score", formatter: rowFormatter, headerSort: false}, - {title:"Max Score", field:"maxScore", formatter: rowFormatter, headerSort: false}, - {title:"Percentage", field:"grade", formatter: rowGradeFormatter, headerSort:false}, + {title: "Category", field: "category", formatter: rowFormatter, headerSort: false}, + {title: "Weight", field: "weight", formatter: weightFormatter, headerSort: false}, + {title: "Score", field: "score", formatter: rowFormatter, headerSort: false}, + {title: "Max Score", field: "maxScore", formatter: rowFormatter, headerSort: false}, + {title: "Percentage", field: "grade", formatter: rowGradeFormatter, headerSort: false}, //filler column to match the assignments table //{title: "", width:1, align:"center", headerSort: false}, { @@ -181,7 +212,7 @@ let categoriesTable = new Tabulator("#categoriesTable", { if (currentFilterRow !== row.getPosition()) { currentFilterRow = row.getPosition(); assignmentsTable.addFilter([ - {field: "category", type:"=", value: row.getData().category} + {field: "category", type: "=", value: row.getData().category} ]); } else { @@ -192,15 +223,15 @@ let categoriesTable = new Tabulator("#categoriesTable", { let mostRecentTable = new Tabulator("#mostRecentTable", { // height: 400, - layout:"fitColumns", + layout: "fitColumns", columns: [ //{title:"Date", field:"date", formatter: rowFormatter, headerSort: false}, - {title:"Date", field:"date", formatter: rowFormatter}, - {title:"Class", field:"classname", formatter: classFormatter}, - {title:"Assignment", field:"assignment", formatter: rowFormatter, headerSort: false}, - {title:"Score", field:"score", formatter: rowFormatter, headerSort: false}, - {title:"Max Score", field:"max_score", formatter: rowFormatter, headerSort: false}, - {title:"Percentage", field:"percentage", formatter: rowGradeFormatter}, + {title: "Date", field: "date", formatter: rowFormatter}, + {title: "Class", field: "classname", formatter: classFormatter}, + {title: "Assignment", field: "assignment", formatter: rowFormatter, headerSort: false}, + {title: "Score", field: "score", formatter: rowFormatter, headerSort: false}, + {title: "Max Score", field: "max_score", formatter: rowFormatter, headerSort: false}, + {title: "Percentage", field: "percentage", formatter: rowGradeFormatter}, ], rowClick: function(e, row) { //trigger an alert message when the row is clicked $("#mostRecentDiv").hide(); @@ -259,28 +290,12 @@ let assignmentsTable = new Tabulator("#assignmentsTable", { { title: "Category", field: "category", - editor:"select", + editor: "select", editorParams: function(cell) { let catCategories = []; - for ( - let k = 0; - k < Object.keys( - currentTableData.currentTermData.classes[selected_class_i].categories - ).length; - k++ - ) { - catCategories.push(( - Object.keys( - currentTableData.currentTermData.classes[selected_class_i].categories - )[k] + " (" + - ( - Object.values( - currentTableData.currentTermData.classes[selected_class_i].categories - )[k] * 100 - ) + "%)" - )); - } + for (let category of Object.keys(currentTableData.currentTermData.classes[selected_class_i].categories)) + catCategories.push(`${category} (${category * 100}%)`); return {values: catCategories}; }, formatter: rowFormatter, @@ -620,9 +635,8 @@ let classesTable = new Tabulator("#classesTable", { .removeAttr("aria-label") .removeAttr("tabindex") .removeClass("hastooltip"); - } - else { - $(`#export_checkbox_terms_${term}`) .attr("disabled", true); + } else { + $(`#export_checkbox_terms_${term}`).attr("disabled", true); $(`#export_checkbox_terms_${term} ~ span`) .attr("aria-label", isAccessibleObj.reason) .attr("tabindex", 0) @@ -647,7 +661,7 @@ let classesTable = new Tabulator("#classesTable", { assignmentsTable.clearFilter(); currentFilterRow = -1; - + document.getElementById("categoriesTable").style.display = "block"; document.getElementById("assignmentsTable").style.display = "block"; selected_class_i = row.getPosition(); @@ -693,18 +707,39 @@ $("#corrections_modal_input").keypress(({ which }) => { correct(); } }); - -/* - * Callback for response from /data +/** + * @typedef {object} Classes + * @param {string} name + * @param {string} grade + * @param {object} categories + * @param {object[]} assignments + * @param {object} tokens + * + * @typedef {object} RecentActivity + * @param {RecentAttendance[]} recentAttendanceArray + * @param {RecentAssignments[]} recentActivityArray + * @param {string} studentName + * + * @typedef {object} scrapedStudent + * @param {Classes[]} response.classes + * @param {RecentActivity} response.recent + * @param {Overview[]} response.overview + * @param {string} response.username + * @param {string} response.quarter + * + * @typedef {object} noLogin response object on no login + * @oaram {boolean} noLogin.nologin - parameter present on login fail + */ + /** Callback for response from /data * - * includedTerms is an optional parameter which contains the terms - * included in an import (in the case that currentTableData is imported - * and not all of the terms' data have been put into currentTableData) + * @param {noLogin|scrapedStudent} response + * @param {(scrapedStudent|string)} includedTerms - optional parameter which contains the terms included in an import (in the case that + * currentTableData is imported and not all of the terms' data have been put into currentTableData) */ function responseCallback(response, includedTerms) { // console.log(response); if (response.nologin) { - tableData = []; + tableData = []; // TODO: dont manipulate global state here, return values currentTableData = undefined; currentTableDataIndex = -1; @@ -715,7 +750,7 @@ function responseCallback(response, includedTerms) { return; } if (response.recent.login_fail) { - location.href='/logout'; + location.href = '/logout'; } if (response.classes.length === 0) { @@ -764,7 +799,7 @@ function responseCallback(response, includedTerms) { $("#loader").hide(); - //parsing the data extracted by the scrappers, and getting tableData ready for presentation + //parsing the data extracted by the scrapers, and getting tableData ready for presentation if (typeof currentTableData.terms === 'undefined') { currentTableData.terms = { current: {}, @@ -818,9 +853,8 @@ function responseCallback(response, includedTerms) { currentTableData.recent.recentActivityArray[i].max_score = currentTableData.currentTermData.classes[temp_classIndex].assignments[assignmentIndex].max_score; currentTableData.recent.recentActivityArray[i].percentage = currentTableData.currentTermData.classes[temp_classIndex].assignments[assignmentIndex].percentage; currentTableData.recent.recentActivityArray[i].color = currentTableData.currentTermData.classes[temp_classIndex].assignments[assignmentIndex].color; - } - catch(err) { - console.log("Please report this error on the Aspine github issue pages. ID Number 101. Error: " + err); + } catch (err) { + console.error("Please report this error on the Aspine github issue pages. ID Number 101. Error: " + err); } } @@ -837,9 +871,9 @@ function responseCallback(response, includedTerms) { } document.getElementById("cum_gpa").innerHTML = "Cumulative GPA: " + currentTableData.cumGPA.percent.toFixed(2); - // Calculate for each quarter + // Calculate GPA for each quarter for (let i = 1; i <= 4; i++) { - currentTableData.terms["q" + i].GPA = computeGPAQuarter(currentTableData.overview,i); + currentTableData.terms["q" + i].GPA = computeGPAQuarter(currentTableData.overview, i); } //Stuff to do now that tableData is initialized @@ -1047,8 +1081,7 @@ function openTab(evt, tab_name) { dataType: "json json", success: pdfCallback }); - } - else if (typeof currentTableData.pdf_files !== 'undefined') { + } else if (typeof currentTableData.pdf_files !== 'undefined') { generate_pdf(pdf_index); } // Redraw PDF to fit new viewport dimensions when transitioning @@ -1060,11 +1093,9 @@ function openTab(evt, tab_name) { }; if (elem.onfullscreenchange !== undefined) { elem.onfullscreenchange = handlefullscreenchange; - } - else if (elem.mozonfullscreenchange !== undefined) { // Firefox + } else if (elem.mozonfullscreenchange !== undefined) { // Firefox elem.mozonfullscreenchange = handlefullscreenchange; - } - else if (elem.MSonfullscreenchange !== undefined) { // Internet Explorer + } else if (elem.MSonfullscreenchange !== undefined) { // Internet Explorer elem.MSonfullscreenchange = handlefullscreenchange; } } diff --git a/scrape.js b/scrape.js index 449d29b9..71947126 100644 --- a/scrape.js +++ b/scrape.js @@ -374,7 +374,7 @@ async function scrape_overview(username, password) { let data = []; $("tr.gradesCell").each(function(i, elem) { // Description Teacher Schedule term Q1 Q2 Q3 Q4 YTD Abs Tdy Dsm - let row = {}; + let row = {}; // TODO: remember how to declare objects cache $(this).children at top or make lamda to remove boilerplate //row["name"] = $(this).find("a").first().text(); //row["category"] = $(this).children().eq(2).text().trim(); row["class"] = $(this).children().eq(0).text().trim(); @@ -396,6 +396,18 @@ async function scrape_overview(username, password) { } // Returns object of recent activity +/** + * @typedef {object} RecentActivity + * @property {string} date + * @property {string} classname + * @property {string} score + * @property {string} assignment + */ +/** + * @param {string} username + * @param {string} password + * @returns {Promise>} + */ async function scrape_recent(username, password) { let session = await scrape_login(); let page = await submit_login(username, password, session.apache_token, session.session_id); @@ -876,6 +888,7 @@ async function scrape_assignments(session_id, apache_token) { while (true) { $("tr.listCell.listRowHeight").each(function(i, elem) { + // TODO: hello fix me let row = {}; //row["name"] = $(this).find("a").first().text(); //row["category"] = $(this).children().eq(2).text().trim(); diff --git a/serve.js b/serve.js index f8bf7ed8..e0111acb 100644 --- a/serve.js +++ b/serve.js @@ -222,6 +222,7 @@ app.post('/data', async (req, res) => { res.send({ nologin: true }); } else { + // TODO add nologin field for consistency res.send(await scraper.scrape_student( req.session.username, req.session.password, req.body.quarter ));