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"