From 10dc9f6def3284b8aa1bef09e915e20e6fcbaa57 Mon Sep 17 00:00:00 2001 From: wen <94116517+wen-wen520@users.noreply.github.com> Date: Sat, 30 Aug 2025 22:33:59 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Calendar=20Export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chrome-extension/calendar/utils/scraper.js | 125 ++++++++++++++++----- 1 file changed, 96 insertions(+), 29 deletions(-) diff --git a/chrome-extension/calendar/utils/scraper.js b/chrome-extension/calendar/utils/scraper.js index 6e5300f..b400989 100644 --- a/chrome-extension/calendar/utils/scraper.js +++ b/chrome-extension/calendar/utils/scraper.js @@ -27,7 +27,9 @@ 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}).`); return { @@ -35,40 +37,105 @@ const weekToJSON = async (dateString = "today") => { 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"); + console.log("[GG DEBUG] meetingElements:", meetingElements); + // list of all the classes this week as HTML elems + + const meetingObjects = []; // Will hold the scraped meeting data + + console.log("[GG DEBUG] meetingElements:", meetingElements); + + // 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..."); 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; + console.log("[GG DEBUG] classDetailsRaw:", classDetailsRaw); + + // 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 + console.log("[GG DEBUG] classDetails:", classDetails); + + // 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; + console.log("[GG DEBUG] courseTitleScrape:", courseTitleScrape); + + // 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, @@ -347,4 +414,4 @@ const scrapeASemester = async (sampleDateString = "today") => { coursesInfo: coursesInfo, additionalMeetings: additionalMeetings, }; // {Array} -}; +}; \ No newline at end of file From c0f0361fd0164dc9f3e2808df2305233171faf34 Mon Sep 17 00:00:00 2001 From: wen <94116517+wen-wen520@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:30:17 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=8D=A5=20Improve=20maintainability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chrome-extension/calendar/utils/scraper.js | 79 +++++++++++++--------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/chrome-extension/calendar/utils/scraper.js b/chrome-extension/calendar/utils/scraper.js index b400989..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 @@ -31,7 +45,7 @@ const weekToJSON = async (dateString = "today") => { 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")), @@ -39,20 +53,15 @@ const weekToJSON = async (dateString = "today") => { } var meetingElements = synchronousMeetings.querySelectorAll(".myu_calendar-class"); - console.log("[GG DEBUG] meetingElements:", meetingElements); - // list of all the classes this week as HTML elems - - const meetingObjects = []; // Will hold the scraped meeting data + const meetingObjects = []; - console.log("[GG DEBUG] meetingElements:", meetingElements); - // 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")) { - console.log("[GG] encountered a no-class meetingElement. skipping..."); + console.log("[GG DEBUG] encountered a no-class meetingElement. skipping..."); continue; } @@ -63,13 +72,11 @@ const weekToJSON = async (dateString = "today") => { continue; } let classDetailsRaw = classDetailsEl.innerHTML; - console.log("[GG DEBUG] classDetailsRaw:", classDetailsRaw); // Clean up HTML to plain text let classDetails = classDetailsRaw .replace(/\n/g, "") .replace(/
/g, "\n"); - console.log("[GG DEBUG] classDetails:", classDetails); // Defensive: Try to get course title element let courseTitleEl = meetingEl.querySelector(".myu_calendar-class-name"); @@ -78,7 +85,6 @@ const weekToJSON = async (dateString = "today") => { continue; } let courseTitleScrape = courseTitleEl.innerText; - console.log("[GG DEBUG] courseTitleScrape:", courseTitleScrape); // Defensive: Try to get color referencer let courseNameEl = meetingEl.querySelector(".myu_calendar-class-name-color-referencer"); @@ -93,11 +99,11 @@ const weekToJSON = async (dateString = "today") => { 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); + // 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"); @@ -144,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 @@ -178,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 @@ -215,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) @@ -237,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 } } } @@ -256,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 @@ -294,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 = {}; @@ -321,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); } } @@ -341,6 +349,7 @@ const scraperSecondPass = (weeks, coursesInfo) => { } } + // console.log("[GG DEBUG] modelWeek: \n") // console.log(modelWeek) return additionalMeetings; }; @@ -352,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 @@ -359,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 From d29ef74a35024954484182083897d7f258723fbf Mon Sep 17 00:00:00 2001 From: wen <94116517+wen-wen520@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:46:15 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=85=20Add=20tooltip=20to=20Calenda?= =?UTF-8?q?r=20Button=20for=20comprehensibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chrome-extension/calendar/calendar.css | 26 ++++++++++++++++++++++++++ chrome-extension/calendar/calendar.js | 1 + 2 files changed, 27 insertions(+) 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 = ` `; From bc0ff95dcf4bf7950eefbbe0fcfed879eeac83e1 Mon Sep 17 00:00:00 2001 From: wen <94116517+wen-wen520@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:48:07 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=86=99=20Update=20the=20extension=20v?= =?UTF-8?q?ersion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chrome-extension/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"