diff --git a/chrome-extension/calendar/calendar.css b/chrome-extension/calendar/calendar.css index 78657cf..2dcb2ed 100644 --- a/chrome-extension/calendar/calendar.css +++ b/chrome-extension/calendar/calendar.css @@ -31,12 +31,38 @@ display: flex; justify-content: space-between; align-items: center; + position: relative; /* Needed for tooltip positioning */ } #gg_gcal_button:hover { opacity: 90%; } +.gg_gcal_tooltip { + visibility: hidden; + opacity: 0.7; + background-color: #5B0013; + box-shadow: #5B0013 0 0 14px; + color: #fff; + text-align: center; + border-radius: 4px; + padding: 4px 8px; + position: absolute; + left: 50%; + top: 40px; + transform: translateX(-50%); + z-index: 10000; + font-size: 13px; + transition: opacity 0.2s; + pointer-events: none; + white-space: nowrap; +} + +#gg_gcal_button:hover .gg_gcal_tooltip { + visibility: visible; + opacity: 1; +} + #cover-spin { position: fixed; width: 100%; diff --git a/chrome-extension/calendar/calendar.js b/chrome-extension/calendar/calendar.js index 4687b7a..20fb2b3 100644 --- a/chrome-extension/calendar/calendar.js +++ b/chrome-extension/calendar/calendar.js @@ -5,6 +5,7 @@ const buttonTemplate = ` `; diff --git a/chrome-extension/calendar/utils/scraper.js b/chrome-extension/calendar/utils/scraper.js index 6e5300f..ca59652 100644 --- a/chrome-extension/calendar/utils/scraper.js +++ b/chrome-extension/calendar/utils/scraper.js @@ -1,9 +1,23 @@ /** + * # Course Data Scraper Utilities + * * Some utilityish functions to scrape course data * The two functions work, but this alone is not yet very useful * Also the functions are still missing some features, (see commented-out lines in JSON objects) + * + * Main Functions: + * - weekToJSON: Scrapes meetings for a given week + * - generalClassInfo: Scrapes general info for a class + * - scraperSecondPass: Cross-references and enhances course/meeting info + * - scrapeASemester: Orchestrates scraping for an entire semester + * + * ORGANIZED: The file is restructured, with original comments retained where useful for context. */ +// ============================ +// Week-Level Scraping +// ============================ + /** * Scrapes a list of json objects containing all the meetings in a given week * Json includes course name, number, date (a single date!), time, room, etc. @@ -17,7 +31,7 @@ const weekToJSON = async (dateString = "today") => { // appends the date info to our base url let baseURL = - "https://www.myu.umn.edu/psp/psprd/EMPLOYEE/CAMP/s/WEBLIB_IS_DS.ISCRIPT1.FieldFormula.IScript_DrawSection?group=UM_SSS§ion=UM_SSS_ACAD_SCHEDULE&pslnk=1&cmd=smartnav"; // the base url + "https://www.myu.umn.edu/psp/psprd/EMPLOYEE/CAMP/s/WEBLIB_IS_DS.ISCRIPT1.FieldFormula.IScript_DrawSection?group=UM_SSS§ion=UM_SSS_ACAD_SCHEDULE&pslnk=1&cmd=smartnav"; let url = baseURL.concat("&effdt=", dateString); // create a (queryable!) DOM element from the url @@ -27,48 +41,107 @@ const weekToJSON = async (dateString = "today") => { .then((r) => (el.innerHTML = r)); // parsing begins - var synchronousMeetings = el.querySelector(".myu_calendar"); // HTML div containing only the classes with set days and times + var synchronousMeetings = el.querySelector(".myu_calendar"); + console.log("[GG DEBUG] synchronousMeetings:", synchronousMeetings); + if (synchronousMeetings == null) { - console.log(`[GG] encountered a week without meetings (${dateString}).`); + console.log(`[GG DEBUG] encountered a week without meetings (${dateString}).`); return { meetingObjects: [], sundayDate: sundayThisWeek(parseDate(dateString, "yyyy-mm-dd")), }; } - var meetingElements = synchronousMeetings.querySelectorAll( - ".myu_calendar-class" - ); // list of all the classes this week as HTML elems - const meetingObjects = []; // list of json objects holding meeting data + + var meetingElements = synchronousMeetings.querySelectorAll(".myu_calendar-class"); + const meetingObjects = []; + + // Loop through each calendar class element for (let meetingEl of meetingElements) { + console.log("[GG DEBUG] Found meetingEl:", meetingEl, meetingEl.innerText); + + // Skip any placeholder for days with no classes if (meetingEl.classList.contains("no-class")) { - // sometimes a meetingEl marks when there are no classes in a day? - console.log("[GG] encountered a no-class meetingElement. skipping..."); + console.log("[GG DEBUG] encountered a no-class meetingElement. skipping..."); continue; } - - let classDetails = meetingEl - .querySelector(".myu_calendar-class-details") - .innerHTML.replace(/\n/g, "") // get rid of random newlines that are in there for some reason + + // Defensive: Try to get class details element + let classDetailsEl = meetingEl.querySelector(".myu_calendar-class-details"); + if (!classDetailsEl) { + console.warn("[GG DEBUG] No .myu_calendar-class-details found for:", meetingEl); + continue; + } + let classDetailsRaw = classDetailsEl.innerHTML; + + // Clean up HTML to plain text + let classDetails = classDetailsRaw + .replace(/\n/g, "") .replace(/
/g, "\n"); - let courseTitleScrape = meetingEl.querySelector( - ".myu_calendar-class-name" - ).innerText; - - meetingObjects.push({ - term: meetingEl.getAttribute("data-strm").trim(), // {string} // in format `xyyx', where `yy` is the year and `xx` = `13` is spring, `19` is fall - courseNum: meetingEl.getAttribute("data-class-nbr").trim(), // {string} - date: parseDate(meetingEl.getAttribute("data-fulldate"), "yyyymmdd"), // {Date} - meetingType: classDetails.match(/^(Lecture)|(Discussion)|(Laboratory)$/m)[0], // {string} // may need updating if list is not exhaustive - timeRange: classDetails.match(/.*:.*/m)[0], // {string} - room: classDetails.match(/^ .* $/m)[0].trim(), // {string} // (room has leading and trailing space) - courseName: meetingEl - .querySelector(".myu_calendar-class-name-color-referencer") - .innerText.trim(), // {string} - institution: meetingEl.dataset.institution, // {string} - prettyName: courseTitleScrape.match(/(?<=\d\) ).*/)[0].trim(), // {string} e.g. "Adv Programming Principles" - sectionID: courseTitleScrape.match(/(?<=\()\d+(?=\))/)[0], // {string} e.g. "001" of "CSCI 2021 (001)" - }); // {MeetingObject} spec + + // Defensive: Try to get course title element + let courseTitleEl = meetingEl.querySelector(".myu_calendar-class-name"); + if (!courseTitleEl) { + console.warn("[GG DEBUG] No .myu_calendar-class-name found for:", meetingEl); + continue; + } + let courseTitleScrape = courseTitleEl.innerText; + + // Defensive: Try to get color referencer + let courseNameEl = meetingEl.querySelector(".myu_calendar-class-name-color-referencer"); + let courseName = courseNameEl ? courseNameEl.innerText.trim() : null; + + // Extract with regex, with fallback if match fails + let meetingTypeMatch = classDetails.match(/^(Lecture|Discussion|Laboratory)/m); + let timeRangeMatch = classDetails.match(/.*:.*/m); + let roomMatch = classDetails.match(/(\n\s*.+\s*\n)/m); // More robust for room line + + let prettyNameMatch = courseTitleScrape.match(/(?<=\d\) ).*/); + let sectionIDMatch = courseTitleScrape.match(/(?<=\()\d+(?=\))/); + + // Print extracted pieces + // console.log("[GG DEBUG] meetingTypeMatch:", meetingTypeMatch); + // console.log("[GG DEBUG] timeRangeMatch:", timeRangeMatch); + // console.log("[GG DEBUG] roomMatch:", roomMatch); + // console.log("[GG DEBUG] prettyNameMatch:", prettyNameMatch); + // console.log("[GG DEBUG] sectionIDMatch:", sectionIDMatch); + + // Defensive: Data attributes + let term = meetingEl.getAttribute("data-strm"); + let courseNum = meetingEl.getAttribute("data-class-nbr"); + let dateRaw = meetingEl.getAttribute("data-fulldate"); + let institution = meetingEl.getAttribute("data-institution"); + + // Defensive: Only add if all required data is present + if ( + term && courseNum && dateRaw && + meetingTypeMatch && timeRangeMatch && roomMatch && + courseName && institution && prettyNameMatch && sectionIDMatch + ) { + // Parse date string as needed + let date = parseDate(dateRaw, "yyyymmdd"); + + meetingObjects.push({ + term: term.trim(), + courseNum: courseNum.trim(), + date: date, + meetingType: meetingTypeMatch[0], + timeRange: timeRangeMatch[0], + room: roomMatch[1].trim(), + courseName: courseName, + institution: institution, + prettyName: prettyNameMatch[0].trim(), + sectionID: sectionIDMatch[0], + }); + + console.log("[GG DEBUG] Added meetingObject:", meetingObjects[meetingObjects.length - 1]); + } else { + console.warn("[GG DEBUG] Skipping meeting due to missing data:", { + term, courseNum, dateRaw, meetingTypeMatch, timeRangeMatch, roomMatch, courseName, institution, prettyNameMatch, sectionIDMatch + }); + } } + + console.log("[GG DEBUG] Final meetingObjects array:", meetingObjects); weekObject = { meetingObjects: meetingObjects, @@ -77,6 +150,10 @@ const weekToJSON = async (dateString = "today") => { return weekObject; }; +// ============================ +// General Class Info Scraping +// ============================ + /** * Scrapes general info on a class: session start, days of week, meeting times, location, etc * @param {string} term @@ -111,12 +188,12 @@ const generalClassInfo = async (term, courseNum, institution = "UMNTC") => { synchronousRow = row; synchronousRowCounter++; } else { - console.log("[GG] found asynchronous meeting in `generalClassInfo`, ignoring..."); + console.log("[GG DEBUG] found asynchronous meeting in `generalClassInfo`, ignoring..."); } } if (synchronousRowCounter != 1) { - console.log(`[GG]!!!!!!!!!!!!!!! ALERT, ${synchronousRowCounter} synchronous meeting found in generalClassInfo() for url ${url}`); + console.log(`[GG DEBUG]!!!!!!!!!!!!!!! ALERT, ${synchronousRowCounter} synchronous meeting found in generalClassInfo() for url ${url}`); } // if this runs, that means we have a big headache and rewrite ahead of us. pls no // dateRange parsing @@ -148,6 +225,10 @@ const generalClassInfo = async (term, courseNum, institution = "UMNTC") => { }; // {CourseObject} spec }; +// ============================ +// Data Enhancement and Cross-Reference +// ============================ + /** * Enhances `coursesInfo` by cross-referencing between it and `weeks`. * Populates `meetingType`, `courseName` (but not `address` yet) @@ -170,6 +251,7 @@ const scraperSecondPass = (weeks, coursesInfo) => { course.sectionID = meeting.sectionID; course.prettyName = meeting.prettyName; continue courseloop; // hah, this is fun! + //that's not such fun actually } } } @@ -189,23 +271,18 @@ const scraperSecondPass = (weeks, coursesInfo) => { */ const timeRangeRepr = (timeRangeString, format) => { let ARBITRARY_DATE = new Date(); + let timeRange; if (format == "week") { - // "hh:mm - hh:mm pm" (first am/pm is ommitted) - timeRange = getTimes(timeRangeString, ARBITRARY_DATE); // long string that is dumb. we will cut it down >:) + timeRange = getTimes(timeRangeString, ARBITRARY_DATE); } else if (format == "coursesInfo") { - // "hh:mm pm - hh:mm pm" - timeRange = recurringGetTimes(timeRangeString, ARBITRARY_DATE); // long string that is dumb + timeRange = recurringGetTimes(timeRangeString, ARBITRARY_DATE); } else { - throw new Error( - `timeRangeRepr() was passed unrecognized format ${format}` - ); + throw new Error(`timeRangeRepr() was passed unrecognized format ${format}`); } return timeRange.map((s) => s.slice(9, 13)).join("-"); // crop off extraneous info }; - // ################################### // First, set up model week hash table - // ################################### let modelWeekHT = {}; // empty json object (hash table) for (day = 0; day < 7; day++) { // for day in a week @@ -227,9 +304,7 @@ const scraperSecondPass = (weeks, coursesInfo) => { let additionalMeetings = []; - // ############################### // Main loop through all the weeks - // ############################### for (let week of weeks) { // Create a hash table for the actual week as well let thisWeekHT = {}; @@ -254,8 +329,8 @@ const scraperSecondPass = (weeks, coursesInfo) => { excludedDate.setDate(excludedDate.getDate() + dayOfWeek); // then shift the date to the correct day of week // console.log(excludedDate) - console.log(`[GG] Excluded meeting found! hash: ${hash}`); - console.log(`[GG] date: ${excludedDate}`) + console.log(`[GG DEBUG] Excluded meeting found! hash: ${hash}`); + console.log(`[GG DEBUG] date: ${excludedDate}`) modelWeekHT[hash].course.excludedDates.push(excludedDate); } } @@ -274,6 +349,7 @@ const scraperSecondPass = (weeks, coursesInfo) => { } } + // console.log("[GG DEBUG] modelWeek: \n") // console.log(modelWeek) return additionalMeetings; }; @@ -285,6 +361,10 @@ const scraperSecondPass = (weeks, coursesInfo) => { return additionalMeetings; }; +// ============================ +// Semester-Level Scraping +// ============================ + // BEGIN ZONE OF EXTRA JANK // Scraping up all meeting times, including catching when classes are canceled for holidays @@ -292,7 +372,7 @@ const scraperSecondPass = (weeks, coursesInfo) => { // 1. pick a sample week (which week best to pick?), then grab all the course numbers in it // 2. Then get the general course info for each of those course numbers, store it somewhere // 3. Then take one of the `dateRange`s (they're all the same) and scrape through the whole thing to find instances of when a class should appear but it doesn't. store this somehow -console.log("[GG] scraper.js runs!"); +console.log("[GG DEBUG] scraper.js runs!"); /** * Given a date, returns info on each course meeting in the semester containing that date. Also returns generally-true info about the courses in the semester @@ -347,4 +427,4 @@ const scrapeASemester = async (sampleDateString = "today") => { coursesInfo: coursesInfo, additionalMeetings: additionalMeetings, }; // {Array} -}; +}; \ No newline at end of file diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json index e0ed348..471586c 100644 --- a/chrome-extension/manifest.json +++ b/chrome-extension/manifest.json @@ -1,7 +1,7 @@ { "name": "Gopher Grades - Past grades for UMN classes!", "description": "Now you can view directly from the course catalog!", - "version": "2.1.0", + "version": "2.2.0", "manifest_version": 3, "background": { "service_worker": "background.js"