diff --git a/my-app/firebase.js b/my-app/firebase.js index 7153d348..07c32454 100644 --- a/my-app/firebase.js +++ b/my-app/firebase.js @@ -35,7 +35,6 @@ export function connectToFirebase(model) { const options = JSON.parse(localStorage.getItem("filterOptions")); if (options) { model.setFilterOptions(options); - console.log("Restore options from local storage"); } reaction( @@ -203,7 +202,6 @@ export async function uploadDepartmentsAndLocations(departments, locations) { const departmentsRef = ref(db, "departments"); try { await set(departmentsRef, departments); - console.log("Uploaded Departments"); } catch (error) { console.error("Failed to upload departments:", error); return false; @@ -213,7 +211,6 @@ export async function uploadDepartmentsAndLocations(departments, locations) { const locationsRef = ref(db, "locations"); try { await set(locationsRef, locations); - console.log("Uploaded Locations"); } catch (error) { console.error("Failed to upload locations:", error); return false; @@ -275,7 +272,6 @@ async function loadCoursesFromCacheOrFirebase(model) { }); if (cachedTimestamp === firebaseTimestamp) { - console.log("Using cached courses from IndexedDB..."); const courseTx = db.transaction("courses", "readonly"); const courseStore = courseTx.objectStore("courses"); const getAllReq = courseStore.getAll(); @@ -291,7 +287,6 @@ async function loadCoursesFromCacheOrFirebase(model) { } // fallback: fetch from Firebase - console.log("Fetching courses from Firebase..."); const courses = await fetchAllCourses(); model.setCourses(courses); saveCoursesToCache(courses, firebaseTimestamp); @@ -304,7 +299,6 @@ export async function addReviewForCourse(courseCode, review) { const updateCourseAvgRating = httpsCallable(functions, 'updateCourseAvgRating'); const result = await updateCourseAvgRating({ courseCode }); - console.log('Average rating updated:', result.data.avgRating); } catch (error) { console.error("Error when adding a course to firebase or updating the average:", error); } diff --git a/my-app/src/model.js b/my-app/src/model.js index 34cbea09..3c67197a 100644 --- a/my-app/src/model.js +++ b/my-app/src/model.js @@ -222,7 +222,6 @@ export const model = { updateLevelFilter(level) { this.filterOptions.level = level; - console.log(level); }, updateDepartmentFilter(department) { @@ -274,7 +273,7 @@ export const model = { }, //for better display we would like the departments in a structured format based on school formatDepartments() { - const grouped = this.departments?.reduce((acc, item) => { + const grouped = this?.departments.reduce((acc, item) => { const [school, department] = item.split("/"); if (!acc[school]) { acc[school] = []; diff --git a/my-app/src/presenters/FilterPresenter.jsx b/my-app/src/presenters/FilterPresenter.jsx index 233c9dd7..40aba878 100644 --- a/my-app/src/presenters/FilterPresenter.jsx +++ b/my-app/src/presenters/FilterPresenter.jsx @@ -6,16 +6,24 @@ import { SearchbarPresenter } from './SearchbarPresenter.jsx'; /* FilterPresenter is responsible for applying the logic necessary to filter out the courses from the overall list */ const FilterPresenter = observer(({ model }) => { /* global variable for the scope of this presenter, all the smaller functions depend on it instead of passing it back and forth as params */ - var localFilteredCourses = []; //might need to declare out of scope. idk js + var localFilteredCourses = []; + /* functions declared here are generally things the main function of this observer takes and runs if the given filters are enabled, + * this is determined through model.filterOptions.apply*Insert filter name* flags. + * This presenter should be changed such that it uses side-effects instead model.filtersChange flag, since + */ + /* functions */ function applyTranscriptEligibility() { if (localFilteredCourses.length == 0) return; - /* */ + /* this should be either weak/moderate/strong */ const eligibilitytype = model.filterOptions.eligibility; + /* I am doing this trick in a multitude of filters, essentially the best fitting courses should appear first in the + * list view on the right side, so we just filter for those and at the very end merge them back together into a single array + */ let strongcourses = []; let zerocourses = []; let moderatecourses = []; @@ -24,7 +32,7 @@ const FilterPresenter = observer(({ model }) => { let storedFinishedCourses = []; if (localStorage.getItem("completedCourses")) - storedFinishedCourses = JSON.parse(localStorage.getItem("completedCourses")); + storedFinishedCourses = JSON.parse(localStorage.getItem("completedCourses")).map(obj => String(obj.id)); localFilteredCourses.forEach(course => { @@ -55,6 +63,7 @@ const FilterPresenter = observer(({ model }) => { }); + /* If user selects strong matching he should get all courses which might be strongly fitting (so strong courses and zero/missing prereq courses) */ switch (eligibilitytype) { case "strong": { @@ -73,7 +82,7 @@ const FilterPresenter = observer(({ model }) => { } default: { - console.log("Error: somehow we got into a state where model.eligibility is no \"strong\"/\"moderate\"/\"weak\"."); + console.log("Error: somehow we got into a state where model.eligibility is not either \"strong\"/\"moderate\"/\"weak\"."); localFilteredCourses = []; break; } @@ -182,8 +191,6 @@ const FilterPresenter = observer(({ model }) => { //course?.language.english (true/false/undefined) //course?.language.swedish (true/false/undefined) - //console.log(data); - switch (languages) { case "none": { @@ -196,8 +203,7 @@ const FilterPresenter = observer(({ model }) => { try { return (course?.language?.english === true); } catch (error) { - console.log(course); - console.log("BIG ERROR", error); + console.log("BIG ERROR", error, course); return false; } @@ -207,8 +213,7 @@ const FilterPresenter = observer(({ model }) => { try { return (course?.language === undefined); } catch (error) { - console.log(course); - console.log("BIG ERROR"); + console.log("BIG ERROR", error, course); return false; } @@ -221,8 +226,7 @@ const FilterPresenter = observer(({ model }) => { try { return (course?.language?.swedish === true); } catch (error) { - console.log(course); - console.log("BIG ERROR"); + console.log("BIG ERROR", error, course); return false; } @@ -232,8 +236,7 @@ const FilterPresenter = observer(({ model }) => { try { return (course?.language === undefined); } catch (error) { - console.log(course); - console.log("BIG ERROR"); + console.log("BIG ERROR", error, course); return false; } @@ -246,8 +249,7 @@ const FilterPresenter = observer(({ model }) => { try { return ((course?.language?.english === true) && (course?.language?.swedish === true)); } catch (error) { - console.log(course); - console.log("BIG ERROR"); + console.log("BIG ERROR", error, course); return false; } @@ -258,8 +260,7 @@ const FilterPresenter = observer(({ model }) => { return (((course?.language?.english === true) && (course?.language?.swedish === false)) || ((course?.language?.english === false) && (course?.language?.swedish === true))); } catch (error) { - console.log(course); - console.log("BIG ERROR"); + console.log("BIG ERROR", error, course); return false; } @@ -269,8 +270,7 @@ const FilterPresenter = observer(({ model }) => { try { return (course?.language === undefined); } catch (error) { - console.log(course); - console.log("BIG ERROR"); + console.log("BIG ERROR", error, course); return false; } @@ -293,31 +293,16 @@ const FilterPresenter = observer(({ model }) => { localFilteredCourses = localFilteredCourses.filter(course => levels.includes(course?.academicLevel)); - /* - let levels = model.filterOptions.level; - let stayingCourses = []; - - for(let i=0; i { try { return (course?.department === undefined); } catch (error) { - console.log("BIG ERROR", error); + console.log("BIG ERROR", error, course); return false; } @@ -337,6 +322,10 @@ const FilterPresenter = observer(({ model }) => { localFilteredCourses = [...bestCourses, ...worstCourses]; } + /* Function that deals with removing the courses that have no properties or have null properties in the categories the user + * using for filtering. The "null" check is a remainder from a version where we didn't use the ?. property accessing yet, + * should be able to be removed without problem in the future. + */ function updateNoNullcourses(){ let local = [...localFilteredCourses]; @@ -380,6 +369,7 @@ const FilterPresenter = observer(({ model }) => { localFilteredCourses = [...local]; } + /* function that should run every single time the model changes (see note below) */ async function run() { if (model.courses.length == 0) { return; @@ -408,17 +398,20 @@ const FilterPresenter = observer(({ model }) => { if (model.filterOptions.applyTranscriptFilter) { applyTranscriptEligibility(); } - if (model.filterOptions.applyDepartments) { + if (model.filterOptions.applyDepartmentFilter) { updateDepartments(); } model.filteredCourses = [...localFilteredCourses]; model.filtersChange = false; model.setFiltersCalculated(); - //console.log("filtered objects number of elements: ", model.filteredCourses.length); } } + /* the problem is that unless using sideeffects, the run() not being async and/or it setting the filterschange = false very early can mean + * that 0 courses will get put into the model.filtered courses (which is the list of courses getting passed to search, and then listview) + * therefore TODO: rework it to stop using this dumb flags we started before learning anything about react,observers,js + */ run(); }); diff --git a/my-app/src/presenters/PrerequisitePresenter.jsx b/my-app/src/presenters/PrerequisitePresenter.jsx index 5361e5b1..a0333b12 100644 --- a/my-app/src/presenters/PrerequisitePresenter.jsx +++ b/my-app/src/presenters/PrerequisitePresenter.jsx @@ -52,7 +52,6 @@ export const PrerequisitePresenter = observer((props) => { const nodeHeight = 36; loadTree(); - //console.log(initialNodes); const getLayoutedElements = (nodes, edges, direction = 'LR') => { const isHorizontal = direction === 'LR'; @@ -237,8 +236,6 @@ export const PrerequisitePresenter = observer((props) => { if (!Array.isArray(current_object)) { // Is object let key = Object.keys(current_object)[0]; - //console.log("Len: " + current_object[key].length); - //console.log("Type: " + typeof current_object[key]); if (current_object[key] != null && current_object[key].length == 1 && (typeof current_object[key][0] == "string" || (current_object[key][0].length == 1 && typeof current_object[key][0][0] == "string"))) { prereq_convert(courses_taken, current_object[key], key, previous_node_id); @@ -281,13 +278,10 @@ export const PrerequisitePresenter = observer((props) => { if (!started_compressing) { if (i < current_object.length - 2 && typeof current_object[i + 1] == "string" && typeof current_object[i + 2] == "string") { let next = current_object[i + 1]; let next_next = current_object[i + 2]; - //console.log(next) - //console.log(course_number, next.slice(2), next_next.slice(2)) if (next.slice(0, 2) === course_letters && next_next.slice(0, 2) === course_letters && !isNaN(next.slice(2)) && !isNaN(next_next.slice(2)) && parseInt(next.slice(2)) == course_number + 1 && parseInt(next_next.slice(2)) == course_number + 2) { - //console.log(course_number, next.slice(2), next_next.slice(2)) current_compresion.push([i, course_letters, course_number]); current_compresion.push([i + 1, course_letters, course_number + 1]); current_compresion.push([i + 2, course_letters, course_number + 2]); @@ -312,7 +306,6 @@ export const PrerequisitePresenter = observer((props) => { } else { started_compressing = false; //refined_course_array.push([i + 1, current_object[i + 1].slice(0, 2), current_object[i + 1].slice(2)]); - //console.log(current_compresion); refined_course_array.push([-1, "!", current_compresion]); current_compresion = []; } @@ -331,8 +324,6 @@ export const PrerequisitePresenter = observer((props) => { } } - //console.log("HERERERERERE!!!") - //console.log(refined_course_array); for (let i = 0; i < refined_course_array.length; i++) { @@ -359,8 +350,6 @@ export const PrerequisitePresenter = observer((props) => { } else if (previous_key == "and" && compressed_done_count == compressed_length) { course_done = true; } - //console.log("Compressed:"); - //console.log(refined_course_array[i][2]); course_code = refined_course_array[i][2][0][1] + refined_course_array[i][2][0][2] + "-" + refined_course_array[i][2][compressed_length - 1][1] + refined_course_array[i][2][compressed_length - 1][2]; input_text = course_code; @@ -394,9 +383,6 @@ export const PrerequisitePresenter = observer((props) => { if (typeof current_object == "object" && !Array.isArray(current_object)) { let key = Object.keys(current_object)[0]; let object_array = current_object[key]; - //console.log("DEBUGGING ") - //console.log(current_node) - //console.log(object_array) let num_of_matches = 0; for (let i = 0; i < object_array.length; i++) { if (Array.isArray(object_array[i])) { @@ -430,15 +416,12 @@ export const PrerequisitePresenter = observer((props) => { } else if(object_array[i] === true) {num_of_matches++} } if (key == "or" && num_of_matches > 0) { - //console.log(current_node) current_object[key] = true; if (current_node != null) { current_node["style"]["backgroundColor"] = "lightgreen"; } } else if (key == "and" && num_of_matches == object_array.length) { - //console.log("DEBUGGING 2"); - //console.log(num_of_matches, object_array.length) current_object[key] = true; if (current_node != null) { current_node["style"]["backgroundColor"] = "lightgreen"; @@ -453,7 +436,6 @@ export const PrerequisitePresenter = observer((props) => { function generateTree(courses_taken, prereqs) { prereq_convert(courses_taken, prereqs, null, props.selectedCourse.code); - //console.log(JSON.stringify(prereqs, null, 4)); let key = Object.keys(prereqs); if (prereqs[key] === true) { return true; @@ -467,7 +449,6 @@ export const PrerequisitePresenter = observer((props) => { function loadTree() { - //console.log(JSON.stringify(props.selectedCourse.prerequisites, null, 4)); if (!props.selectedCourse?.prerequisites || props.selectedCourse.prerequisites.length == 0) { let display_node = createNode("No Prerequisites", "No Prerequisites", "default"); display_node.style["pointerEvents"] = "none"; diff --git a/my-app/src/presenters/SidebarPresenter.jsx b/my-app/src/presenters/SidebarPresenter.jsx index a75a61ec..04876ed9 100644 --- a/my-app/src/presenters/SidebarPresenter.jsx +++ b/my-app/src/presenters/SidebarPresenter.jsx @@ -1,19 +1,22 @@ import React, { useEffect } from 'react'; import { observer } from "mobx-react-lite"; import SidebarView from "../views/SidebarView.jsx"; - +import transcriptScraperFunction from "./UploadTranscriptPresenter.jsx"; +import { useState } from "react"; const SidebarPresenter = observer(({ model }) => { - - useEffect(() => { - model.setFiltersChange(); - }); + let currentLanguageSet = model.filterOptions.language; let currentLevelSet = model.filterOptions.level; let currentPeriodSet = model.filterOptions.period; let currentDepartmentSet = model.filterOptions.department; - let currentLocationSet = model.filterOptions.location + let currentLocationSet = model.filterOptions.location; + + + useEffect(() => { + model.setFiltersChange(); + },[]); function handleLanguageFilterChange(param) { if (param === "English") { @@ -124,7 +127,6 @@ const SidebarPresenter = observer(({ model }) => { break; case "department": handleDepartmentFilterChange(param[2]); - console.log(param[2]); break; case "period": handlePeriodFilterChange(param[2]); @@ -144,31 +146,24 @@ const SidebarPresenter = observer(({ model }) => { function HandleFilterEnable(param) { switch (param[0]) { case "language": - console.log("language filter set to: " + param[1]); model.setApplyLanguageFilter(param[1]); break; case "level": - console.log("level filter set to: " + param[1]); model.setApplyLevelFilter(param[1]); break; case "location": - console.log("location filter set to: " + param[1]); model.setApplyLocationFilter(param[1]); break; case "credits": - console.log("credits filter set to: " + param[1]); model.setApplyCreditsFilter(param[1]); break; case "transcript": - console.log("transcript filter set to: " + param[1]); model.setApplyTranscriptFilter(param[1]); break; case "department": - console.log("department filter set to: " + param[1]); model.setApplyDepartmentFilter(param[1]); break; case "period": - console.log("period filter set to: " + param[1]); model.setApplyPeriodFilter(param[1]); break; default: @@ -184,6 +179,55 @@ const SidebarPresenter = observer(({ model }) => { model.setApplyRemoveNullCourses(); } + function formatDepartmentSet(departmentSet) { + const grouped = departmentSet.reduce((acc, item) => { + if (item.includes("/")) { + const [school, department] = item.split("/"); + if (!acc[school]) { + acc[school] = []; + } + acc[school].push(department?.trim()); + return acc; + } else { + const [school] = item; + if (!acc[school]) { + acc[school] = []; + } + return acc; + } + }, {}); + const sortedGrouped = Object.keys(grouped) + .sort() + .reduce((acc, key) => { + acc[key] = grouped[key].sort(); + return acc; + }, {}); + const fields = Object.entries(sortedGrouped).map(([school, departments], index) => ({ + id: index + 1, + label: school, + subItems: departments, + })); + return fields; + } + + //========================================================== + + const [errorMessage, setErrorMessage] = useState(""); // Stores error message + const [errorVisibility, setErrorVisibility] = useState("hidden"); // Controls visibility + const [fileInputValue, setFileInputValue] = useState(""); // Controls upload field state + + const handleFileChange = (event) => { + const truncatedCourses = model.courses.map(({ id, name }) => ({ id, name })); + const file = event.target.files[0]; + //document.getElementById('PDF-Scraper-Error').style.visibility = "visible"; + transcriptScraperFunction(file, setErrorMessage, setErrorVisibility, reApplyFilter, truncatedCourses); + //document.getElementById('PDF-Scraper-Input').value = ''; + setFileInputValue(''); + + + }; + //========================================================== + return ( { initialPeriodFilterOptions={currentPeriodSet} initialPeriodFilterEnable={model.filterOptions.applyPeriodFilter} - initialDepartmentFilterOptions={currentDepartmentSet} + initialDepartmentFilterOptions={formatDepartmentSet(currentDepartmentSet.filter(item => item !== "undefined/undefined"))} initialDepartmentFilterEnable={model.filterOptions.applyDepartmentFilter} DepartmentFilterField = {model.formatDepartments()} @@ -214,6 +258,15 @@ const SidebarPresenter = observer(({ model }) => { initialCreditsFilterEnable={model.filterOptions.applyCreditsFilter} initialApplyNullFilterEnable={model.filterOptions.applyRemoveNullCourses } + + //========================================================== + + PdfErrorMessage={errorMessage} + PdfErrorVisibility={errorVisibility} + PdfHandleFileChange={handleFileChange} + PdfFileInputValue={fileInputValue} + + //========================================================== /> ); }); diff --git a/my-app/src/presenters/UploadTranscriptPresenter.jsx b/my-app/src/presenters/UploadTranscriptPresenter.jsx index 63bd0148..4d73eb16 100644 --- a/my-app/src/presenters/UploadTranscriptPresenter.jsx +++ b/my-app/src/presenters/UploadTranscriptPresenter.jsx @@ -1,197 +1,204 @@ -import React from 'react'; -import { observer } from "mobx-react-lite"; import * as pdfjsLib from "pdfjs-dist"; import pdfWorker from "pdfjs-dist/build/pdf.worker?url"; -import { useState } from "react"; -import UploadField from '../views/Components/SideBarComponents/UploadField'; -pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker; +/* This file originally was the TranscriptScraper.js but it got reworked into a sort of presenter which was under a view for some reason, + * and was the one who called the UploadField component (something that a view should've done). So this in the last sprint got hastily + * refactored such that its organisation makes atleast some sense in the MVP model. That caused it to be such a weird function with 4 methods being + * passed in its arguments. Now this is used as a source file in SidebarPresenter instead, but still requires quite some cleaning up. + */ -const UploadTranscriptPresenter = observer((props) => { - const [errorMessage, setErrorMessage] = useState(""); // Stores error message - const [errorVisibility, setErrorVisibility] = useState("hidden"); // Controls visibility - const [fileInputValue, setFileInputValue] = useState(""); // Controls upload field state - - async function transcriptScraperFunction(file) { - //console.log(file); - //const pdfjsLib = window['pdfjsLib']; - //pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js'; - if (!file) { - console.log("element: 'PDF-Scraper-Input' changed, but we havent gotten a file yet."); - return; - } - if (file.type !== "application/pdf") { - throwTranscriptScraperError("Uploaded file isn't PDF."); - return; - } - - setErrorVisibility("hidden"); +pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker; +export default async function transcriptScraperFunction(file, setErrorMessage, setErrorVisibility, reApplyFilter, courseList) { + if (!file) { + console.log("element: 'PDF-Scraper-Input' changed, but we havent gotten a file yet."); + return; + } + if (file.type !== "application/pdf") { + throwTranscriptScraperError("Uploaded file isn't PDF.", setErrorMessage, setErrorVisibility); + return; + } - const arrayBuffer = await file.arrayBuffer(); - const typedArray = new Uint8Array(arrayBuffer); - try { - const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise; + setErrorVisibility("hidden"); - //this is our array we are going to work with - let textObjects = []; + const arrayBuffer = await file.arrayBuffer(); + const typedArray = new Uint8Array(arrayBuffer); + try { + const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise; - //we will parse the whole pdf page-by-page, and going to push all the content into our array - for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { - const page = await pdf.getPage(pageNum); - const textContent = await page.getTextContent(); - //pushing all the text items from the page into our array - textObjects.push(...textContent.items); - } + //this is our array we are going to work with + let textObjects = []; - evaluatePDFtextObjectArray(textObjects); + //we will parse the whole pdf page-by-page, and going to push all the content into our array + for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { + const page = await pdf.getPage(pageNum); + const textContent = await page.getTextContent(); + //pushing all the text items from the page into our array + textObjects.push(...textContent.items); + } + evaluatePDFtextObjectArray(textObjects, setErrorMessage, setErrorVisibility, courseList); - //document.getElementById('transcript-scraper.js:output').textContent = localStorage.getItem("completedCourses") || 'No matching text found.'; - } - catch (e) { - throwTranscriptScraperError("While parsing the pdf something went wrong." + e); - } } - - function throwTranscriptScraperError(txt) { - console.log("PDF-Scraper-Error: " + txt); - setErrorMessage("Error: " + txt); - setErrorVisibility("visible"); + catch (e) { + throwTranscriptScraperError("While parsing the pdf something went wrong.\n" + e, setErrorMessage, setErrorVisibility); + console.log(e); + } + reApplyFilter(); +} + +function throwTranscriptScraperError(txt, setErrorMessage, setErrorVisibility) { + console.log("PDF-Scraper-Error: " + txt); + setErrorMessage("Error: " + txt); + setErrorVisibility("visible"); +} + +function writeLocalStorage_completedCourses(coursePairs) { + //Getting the local storage contents + let local = []; + if (localStorage.getItem("completedCourses")) + local = JSON.parse(localStorage.getItem("completedCourses")); + else { + localStorage.setItem("completedCourses", '[]'); } - function writeLocalStorage_completedCourses(codesArr) { - //Getting the local storage contents - let local = []; - if (localStorage.getItem("completedCourses")) - local = JSON.parse(localStorage.getItem("completedCourses")); - else { - localStorage.setItem("completedCourses", '[]'); - } - - local.sort(); - - let newcodes = local.concat(codesArr); - newcodes = [... new Set(newcodes)]; - - - localStorage.setItem("completedCourses", JSON.stringify(newcodes)); - //console.log(newcodes); + //sorting objects by id + //local.sort((a, b) => a.id.localeCompare(b.id)); - window.dispatchEvent(new Event("completedCourses changed")); + const map = new Map(coursePairs.map(course => [course.id, course])); + for(const course of local){ + map.set(course.id, course); } - function evaluatePDFtextObjectArray(textObjects) { - let scrapedCodes = []; - - //initializing couple flags. - let flagKTH = false; - let flagKTH_NeverSet = true; - let flagTable = false; - let flagTableDone = false; - - let flagErrorRecords = false; - - //we are going to go through each text object which is inside the pdf file. - for (let i = 0; i < textObjects.length; i++) { - //we are going to look for our university, KTH - //current ladok generated National Official transcripts start at xposition 56.692 - if ((!flagKTH) && (textObjects[i].transform[4] === 56.692)) - if ((textObjects[i].str == "Kungliga Tekniska högskolan") || (textObjects[i].str == "KTH Royal Institute of Technology")) { - flagKTH = true; - flagKTH_NeverSet = false; - continue; - } - - if ((!flagErrorRecords) && ((textObjects[i].str == "Resultatintyg") || (textObjects[i].str == "Official Transcript of Records"))) { - flagErrorRecords = true; + let newcodes = Array.from(map.values()); + newcodes.sort((a, b) => a.id.localeCompare(b.id)); + + + localStorage.setItem("completedCourses", JSON.stringify(newcodes)); + + window.dispatchEvent(new Event("completedCourses changed")); +} + +function evaluatePDFtextObjectArray(textObjects, setErrorMessage, setErrorVisibility, courseList) { + let scrapedCoursesbyID = []; + + // theres a new structure to scraped courses by ID + /* + [{ + id: "IK1203", + name: "Nätverk och kommunikation", //can be swedish or english depending on transcript provided + }, + ..., {...}}] + */ + + //initializing couple flags. + let flagKTH = false; + let flagKTH_NeverSet = true; + let flagTable = false; + let flagTableDone = false; + let flagErrorRecords = false; + + //we are going to go through each text object which is inside the pdf file. + for (let i = 0; i < textObjects.length; i++) { + //we are going to look for our university, KTH + //current ladok generated National Official transcripts start at xposition 56.692 + if ((!flagKTH) && (textObjects[i].transform[4] === 56.692)) + if ((textObjects[i].str == "Kungliga Tekniska högskolan") || (textObjects[i].str == "KTH Royal Institute of Technology")) { + flagKTH = true; + flagKTH_NeverSet = false; + continue; } - if (flagKTH) { - //we have found KTH, the very next table containing records should be the one with completed courses - //TODO: this might not be necessarily true, you might need to have a similar code to KTH checker, to check if its - // 'completed courses'/'avslutade kurser' - - - //the very first text in a table is always Code/Kod; we will start describing it; and we will detect when a new table starts - //and check if its accidentally the same table which just got cut in half by a newline or an actually different table - if ((textObjects[i].str === "Code") || (textObjects[i].str === "Kod")) { - if (flagTable) flagTableDone = true; //we have already found one table and transcribed it - - if (!flagTableDone) { - flagTable = true; - } else { - if ((textObjects[i - 1].transform[4] !== 532.71801758) && (textObjects[i - 11].transform[4] !== 532.71801758)) { - //if its i-1, the page number is the object directly behind, otherwise if -11 its because theres some filter, - //e.g. utskrift datum, personnummer and others. hopefully this should cover all base (probably doesn't) - //this is a very hardcoded solution to this problem. - //the new table (that is the new found "Kod" / "Code" is not because unexpected page break, therefore we are done transcribing - //KTH courses, these are either uncomplete courses, or courses from other universities - flagTable = false; - //console.log("----------------------------\nfinished table!"); - flagKTH = false; - } + if ((!flagErrorRecords) && ((textObjects[i].str == "Resultatintyg") || (textObjects[i].str == "Official Transcript of Records"))) { + flagErrorRecords = true; + } + + if (flagKTH) { + //we have found KTH, the very next table containing records should be the one with completed courses + //TODO: this might not be necessarily true, you might need to have a similar code to KTH checker, to check if its + // 'completed courses'/'avslutade kurser' + + + //the very first text in a table is always Code/Kod; we will start describing it; and we will detect when a new table starts + //and check if its accidentally the same table which just got cut in half by a newline or an actually different table + if ((textObjects[i].str === "Code") || (textObjects[i].str === "Kod")) { + if (flagTable) flagTableDone = true; //we have already found one table and transcribed it + + if (!flagTableDone) { + flagTable = true; + } else { + if ((textObjects[i - 1].transform[4] !== 532.71801758) && (textObjects[i - 11].transform[4] !== 532.71801758)) { + //if its i-1, the page number is the object directly behind, otherwise if -11 its because theres some filter, + //e.g. utskrift datum, personnummer and others. hopefully this should cover all base (probably doesn't) + //this is a very hardcoded solution to this problem. + //the new table (that is the new found "Kod" / "Code" is not because unexpected page break, therefore we are done transcribing + //KTH courses, these are either uncomplete courses, or courses from other universities + flagTable = false; + //we have finished the table! + flagKTH = false; } } - //we are looking for text objects which are precisely at x coord 56.692; and also there exists such an element 12 ahead in the array - //which is at coord 510.233; these are hardcoded values into the ladok pdf generator - //for good measures we also make sure the text is not longer that 7 chars; the longest course ID found so far at KTH - if ((textObjects[i].transform[4] === 56.692) && (textObjects[i + 12].transform[4] === 510.233) && (textObjects[i].str.length < 8)) - if (flagTable) { - //console.log(textObjects[i].str, textObjects[i].transform[4]); - //extractedText+= textObjects[i].str + "\n"; - scrapedCodes.push(textObjects[i].str); - } - } + //we are looking for text objects which are precisely at x coord 56.692; and also there exists such an element 12 ahead in the array + //which is at coord 510.233; these are hardcoded values into the ladok pdf generator + //for good measures we also make sure the text is not longer that 7 chars; the longest course ID found so far at KTH + if ((textObjects[i].transform[4] === 56.692) && (textObjects[i + 12].transform[4] === 510.233) && (textObjects[i].str.length < 8)) + if (flagTable) { + let scrapedObj = { + id: textObjects[i].str, + name: textObjects[i+2].str + }; + scrapedCoursesbyID.push(scrapedObj); + } } - if (flagErrorRecords && (scrapedCodes.length == 0)) { - throwTranscriptScraperError("Provided Official Transcript of Records instead of National Official transcript of records."); - return; - } + } - if (flagKTH_NeverSet) { - throwTranscriptScraperError("Provided pdf doesn't contain KTH."); - return; - } - //console.log(scrapedCodes); - //console.log(localStorage.getItem("completedCourses")); - if (scrapedCodes.length == 0) { - throwTranscriptScraperError("Couldn't find any tables to transcribe."); - return; - } - writeLocalStorage_completedCourses(scrapedCodes); - //console.log(localStorage.getItem("completedCourses")); + if (flagErrorRecords && (scrapedCoursesbyID.length == 0)) { + throwTranscriptScraperError("Provided Official Transcript of Records instead of National Official transcript of records.", setErrorMessage, setErrorVisibility); + return; + } + + if (flagKTH_NeverSet) { + throwTranscriptScraperError("Provided pdf doesn't contain KTH.", setErrorMessage, setErrorVisibility); + return; + } - props.reApplyFilter(); + if (scrapedCoursesbyID.length == 0) { + throwTranscriptScraperError("Couldn't find any tables to transcribe.", setErrorMessage, setErrorVisibility); + return; } - const handleFileChange = (event) => { - const file = event.target.files[0]; - //document.getElementById('PDF-Scraper-Error').style.visibility = "visible"; - transcriptScraperFunction(file); - //document.getElementById('PDF-Scraper-Input').value = ''; - setFileInputValue(''); - }; - - return ( - ); -}); - -export { UploadTranscriptPresenter }; \ No newline at end of file + let coursePairs = []; + scrapedCoursesbyID.forEach(scraped_course => { + let obj = { ...courseList.find(course => course.id == scraped_course.id), is_in_DB: true}; + if(obj?.name !== undefined){ + coursePairs.push(obj); + } + else + coursePairs.push({...scraped_course, is_in_DB: false}); + }); + + try { + writeLocalStorage_completedCourses(coursePairs); + } catch (e) { + console.log(e); + } + + +} + +/* + Structure we are storing our completed courses in LocalStorage. + [ + { + id: , //str + name: , //str + is_in_DB: //true/false + } + ] +*/ \ No newline at end of file diff --git a/my-app/src/scripts/eligibility_refined.js b/my-app/src/scripts/eligibility_refined.js index b164f1db..411b12a4 100644 --- a/my-app/src/scripts/eligibility_refined.js +++ b/my-app/src/scripts/eligibility_refined.js @@ -9,12 +9,10 @@ function prereq_convert(courses_taken, current_object, previous_key, hash_bool, if (typeof current_object[i] == "string") { if (current_object[i][0] == '#') { current_object[i] = hash_bool; - //console.log(JSON.stringify(prereqs, null, 4)); } else if (courses_taken.includes(current_object[i])) { current_object[i] = true; if (count_object != undefined) {count_object["count"]++;} } else { - //console.log(current_object[i]) current_object[i] = false; if (count_object != undefined) {count_object["count"]++;} } @@ -28,11 +26,8 @@ function prereq_convert(courses_taken, current_object, previous_key, hash_bool, if (typeof current_object == "object" && !Array.isArray(current_object)) { let key = Object.keys(current_object)[0]; let object_array = current_object[key]; - //console.log(key); - //console.log(object_array); let num_of_matches = 0; for (let i = 0; i < object_array.length; i++) { - //console.log(current_object[i]) if (Array.isArray(object_array[i])) { let num_of_inner_matches = 0; for (let j = 0; j < object_array[i].length; j++) { @@ -40,7 +35,6 @@ function prereq_convert(courses_taken, current_object, previous_key, hash_bool, num_of_inner_matches ++; } } - //console.log("key: " + key + " num of matc ") if (key == "or" && num_of_inner_matches > 0) {object_array[i] = true; num_of_matches++; continue;} if (key == "and" && num_of_inner_matches == object_array[i].length) {object_array[i] = true; num_of_matches++; continue;} object_array[i] = false; @@ -49,15 +43,9 @@ function prereq_convert(courses_taken, current_object, previous_key, hash_bool, if (object_array[i][inner_key]) {num_of_matches++;} } else if(object_array[i]) {num_of_matches++} } - //console.log(num_of_matches); if (key == "or" && num_of_matches > 0) {current_object[key] = true} else if (key == "and" && num_of_matches == object_array.length) {current_object[key] = true} else {current_object[key] = false} - - - - //console.log(JSON.stringify(prereqs, null, 4)); - } } diff --git a/my-app/src/views/Components/SideBarComponents/CollapsibleCheckboxes.jsx b/my-app/src/views/Components/SideBarComponents/CollapsibleCheckboxes.jsx index 8ca4aadd..dbf39957 100644 --- a/my-app/src/views/Components/SideBarComponents/CollapsibleCheckboxes.jsx +++ b/my-app/src/views/Components/SideBarComponents/CollapsibleCheckboxes.jsx @@ -3,10 +3,11 @@ import FilterEnableCheckbox from "./FilterEnableCheckbox"; import Tooltip from "./ToolTip"; const CollapsibleCheckboxes = (props) => { - const [expanded, setExpanded] = useState({}); + const [expandedLabel, setExpandedLabel] = useState(props?.initialValues?.map(i => i?.label)); + const [expanded, setExpanded] = useState([]); const [filterEnabled, setFilterEnabled] = useState(props.filterEnable); const [checkedSubItems, setCheckedSubItems] = useState({}); - const [stupidLines, setStupidLines] = useState(0); + const [initalLoad, setInitalLoad] = useState(true); const strokeWidth = 5; @@ -16,45 +17,75 @@ const CollapsibleCheckboxes = (props) => { const rows = props.fields; - const toggleExpand = (id, subItems) => { + const toggleExpand = (id, subItems) => { + let entry = rows.find(item => item.id === id); + if (!entry?.subItems || (entry?.subItems.length == 1 && !entry?.subItems[0])) { + props.HandleFilterChange([paramFieldType, props.filterName, + entry?.label + ]); + }else if (entry && entry?.label && entry?.subItems) { + subItems?.map((_, index) => { + if ((checkedSubItems[`${id}-${index}`] && expanded[id]) || (!expanded[id])) { + props.HandleFilterChange([paramFieldType, props.filterName, + entry?.label + "/" + entry?.subItems[index] + + ]); + } + + setSubCheckbox(id, index); + }); + } setExpanded((prev) => ({ ...prev, [id]: !prev[id], })); - let label = rows.find(item => item.id === id).label; - subItems.map((_, index) => { - setSubCheckbox(id, index) - if (rows.find(item => item.id === id)?.subItems && rows.find(item => item.id === id)?.subItems.length>0 && rows.find(item => item.id === id)?.subItems[0]) { - props.HandleFilterChange([paramFieldType, props.filterName, - label + "/" + rows.find(item => item.id === id).subItems[index] - ]); - } else { - props.HandleFilterChange([paramFieldType, props.filterName, - label - ]); - } - }); }; const setSubCheckbox = (mainId, index) => { const key = `${mainId}-${index}`; setCheckedSubItems((prev) => ({ ...prev, - [key]: true, + [key]: ((expanded[mainId] && prev[key]) || !expanded[mainId]) ? !prev[key]:false, })); }; const toggleSubCheckbox = (mainId, index) => { const key = `${mainId}-${index}`; + props.HandleFilterChange([paramFieldType, props.filterName, + rows.find(item => item.id === mainId)?.label + "/" + rows.find(item => item.id === mainId)?.subItems[index] + ]); setCheckedSubItems((prev) => ({ ...prev, [key]: !prev[key], })); - props.HandleFilterChange([paramFieldType, props.filterName, - rows.find(item => item.id === mainId).label + "/" + rows.find(item => item.id === mainId).subItems[index] - ]); }; + React.useEffect(() => { + if (rows && rows.length > 0 && initalLoad) { + let tempExpanded = {}; + let tempChecked = {}; + rows.forEach(r => r.subItems.forEach((_, index) => { + tempChecked[`${r.id}-${index}`] = false; + })); + props?.initialValues?.forEach(i => { + let mainRow = rows.find((item) => item.label === i.label); + if (mainRow) { + tempExpanded[mainRow.id] = true; + i.subItems.forEach(s => { + let subItemIndex = mainRow.subItems.findIndex(item => item === s); + if (subItemIndex !== -1) { + tempChecked[`${mainRow.id}-${subItemIndex}`] = true; + } + }); + } + }); + setCheckedSubItems(tempChecked); + setInitalLoad(false); + setExpanded(tempExpanded); + } + }, [rows]); + + return (
@@ -183,8 +214,8 @@ const CollapsibleCheckboxes = (props) => { checked={!!checkedSubItems[key]} onChange={() => toggleSubCheckbox(row.id, index)} /> -
); diff --git a/my-app/src/views/Components/SideBarComponents/CourseTranscriptList.jsx b/my-app/src/views/Components/SideBarComponents/CourseTranscriptList.jsx index 642c6f7b..dea19157 100644 --- a/my-app/src/views/Components/SideBarComponents/CourseTranscriptList.jsx +++ b/my-app/src/views/Components/SideBarComponents/CourseTranscriptList.jsx @@ -1,10 +1,13 @@ -import { useState } from "react"; +import { useState, useRef, forwardRef } from "react"; +import Tooltip from "./ToolTip"; export default function CourseTranscriptList(props) { let local = []; if (localStorage.getItem("completedCourses")) local = JSON.parse(localStorage.getItem("completedCourses")); - + + const [items, setItems] = useState(local); + // eslint-disable-next-line no-unused-vars window.addEventListener("completedCourses changed", event => { if (localStorage.getItem("completedCourses")) @@ -13,8 +16,6 @@ export default function CourseTranscriptList(props) { }); - const [items, setItems] = useState(local); - function removeItem(index) { var newItems = []; for (let i = 0; i < items.length; i++) { @@ -30,12 +31,38 @@ export default function CourseTranscriptList(props) { localStorage.setItem("completedCourses", JSON.stringify(newitems)); window.dispatchEvent(new Event("completedCourses changed")); props.reApplyFilter(); + if (props.checkboxRef && props.checkboxRef.current) { + props.checkboxRef.current.click(); + } }; + //===================================== + const tooltipRef = useRef(null); + + const tooltipClasses = [ + "absolute", + "bg-gray-800", + "text-white", + "text-xs", + "rounded-xl", + "z-100", + "px-3", + "py-2", + "opacity-0", + "peer-hover:opacity-100", + "transition-opacity", + "duration-200", + "pointer-events-none", + "break-words", + "shadow-lg", + ].join(" "); + + //===================================== + return ( -
+
{items.length > 0 && (
@@ -51,39 +78,45 @@ export default function CourseTranscriptList(props) {
-
- {items.map((item, index) => ( -
+ {items.map((item, index) => ( +
+
+ {item?.id} + +
+ {item?.name + (item?.is_in_DB ? "" : " (Course discontinued)")} +
+
+ -
- ))} -
+ + + + +
+ ))} +
); diff --git a/my-app/src/views/Components/SideBarComponents/DropDownField.jsx b/my-app/src/views/Components/SideBarComponents/DropDownField.jsx index b416a4e2..883d8f82 100644 --- a/my-app/src/views/Components/SideBarComponents/DropDownField.jsx +++ b/my-app/src/views/Components/SideBarComponents/DropDownField.jsx @@ -9,9 +9,10 @@ export default function DropDownField(props) { let paramFieldType = "dropdown"; const [filterEnabled, setFilterEnabled] = useState(props.filterEnable); const [isOpen, setIsOpen] = useState(false); - const [selectedItems, setSelectedItems] = useState(props.initialValues.map(t => t?.toUpperCase())); + const [selectedItems, setSelectedItems] = useState(props?.initialValues?.map(t => t?.toUpperCase())); + + const items = props?.options?.map(t => t?.toUpperCase()); - const items = props.options.map(t => t?.toUpperCase()); const checkboxRef = useRef(null); @@ -19,7 +20,7 @@ export default function DropDownField(props) { const handleCheckboxChange = (item) => { setSelectedItems((prev) => - prev.includes(item?.toUpperCase()) ? prev.filter((i) => i !== item) : [...prev, item] + prev?.includes(item?.toUpperCase()) ? prev.filter((i) => i !== item) : [...prev, item] ); props.HandleFilterChange([paramFieldType, props.filterName, item]); }; @@ -69,14 +70,14 @@ export default function DropDownField(props) { onClick={toggleDropdown} className="bg-violet-500 text-white px-4 py-2 rounded-md shadow-md focus:outline-none hover:bg-[#aba8e0] w-full" > - {selectedItems.length? (selectedItems.map(i => String(i).charAt(0).toUpperCase() + String(i).slice(1).toLowerCase()).join(", ").substring(0, 30) + ((selectedItems.join(", ").substring(0, 30).length>=30)? "...": "") ):"Select Options"} + {selectedItems?.length? (selectedItems?.map(i => String(i).charAt(0).toUpperCase() + String(i).slice(1).toLowerCase()).join(", ").substring(0, 30) + ((selectedItems?.join(", ").substring(0, 30).length>=30)? "...": "") ):"Select Options"} {/* Dropdown Menu */} {isOpen && (
    - {items.map((item, index) => ( + {items?.map((item, index) => (
-
+
{ setFilterEnabled(!filterEnabled); props.HandleFilterEnable(["transcript", !filterEnabled]); - }} /> + }} + />
-
{ +
+
{ if (!filterEnabled && checkboxRef.current) { checkboxRef.current.click(); } @@ -60,16 +65,26 @@ export default function UploadField(props) {
-
diff --git a/my-app/src/views/SidebarView.jsx b/my-app/src/views/SidebarView.jsx index 4130c9bd..0f4a0460 100644 --- a/my-app/src/views/SidebarView.jsx +++ b/my-app/src/views/SidebarView.jsx @@ -2,9 +2,10 @@ import React from 'react'; import ToggleField from "./Components/SideBarComponents/ToggleField.jsx"; import SliderField from "./Components/SideBarComponents/SliderField.jsx"; import DropDownField from "./Components/SideBarComponents/DropDownField.jsx"; -import { UploadTranscriptPresenter } from '../presenters/UploadTranscriptPresenter.jsx'; +//import { UploadTranscriptPresenter } from '../presenters/UploadTranscriptPresenter.jsx'; import CollapsibleCheckboxes from './Components/SideBarComponents/CollapsibleCheckboxes.jsx'; import Tooltip from './Components/SideBarComponents/ToolTip.jsx'; +import UploadField from './Components/SideBarComponents/UploadField'; import MultipleChoiceButtons from './Components/SideBarComponents/MultipleChoiceButtons.jsx'; @@ -21,14 +22,26 @@ function SidebarView(props) {
Filters
- + />*/} +
@@ -48,6 +61,7 @@ function SidebarView(props) { initialValues={props.initialLanguageFilterOptions} filterEnable = {props.initialLanguageFilterEnable} HandleFilterEnable={props.HandleFilterEnable} + description="Filter language. If you select both, courses which are offered both in English and Swedish are going to be on the top." /> @@ -58,7 +72,8 @@ function SidebarView(props) { initialValues={props.initialPeriodFilterOptions} filterEnable = {props.initialPeriodFilterEnable} HandleFilterEnable={props.HandleFilterEnable} - description="Filter language. If you select both, courses which are offered both in English and Swedish are going to be on the top." + description="Filter by the period a course is given, the autumn semester consists of P1 and P2, while the spring semester is P3 and P4. + Courses offered over multiple periods will also show up." />