first commit

This commit is contained in:
Kyler Olsen 2025-07-11 01:06:08 -06:00
commit 006e86da56
5 changed files with 433 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
final_exams.html
*.json

94
download.py Normal file
View File

@ -0,0 +1,94 @@
import requests
import bs4
import datetime
import json
# This script downloads the final exam schedule from Snow College's registrar page
# and parses it to extract the semester and exam times for each course.
# Last tested for Fall 2025 on 2025-07-10
def parse_days(days_str):
# Mon, Wed, or Fri
# Tue or Thu
days = days_str.split(" ")
days = [day.strip(',') for day in days if day.strip(',')]
day_map = {
"Mon": "M",
"Tue": "T",
"Wed": "W",
"Thu": "Th",
"Fri": "F",
}
return [day_map[day] for day in days if day in day_map]
def parse_class_time(time_str) -> tuple[datetime.time, datetime.time]:
# 7:00 or 7:30am
# *5:00pm or later
am_or_pm = "am" if "am" in time_str else "pm"
time_parts = time_str.replace("*", "").replace(am_or_pm, "").split(" ")
start_time = datetime.datetime.strptime(time_parts[0] + am_or_pm, "%I:%M%p").time()
if "later" in time_str:
end_time = datetime.time(23, 59)
else:
end_time = datetime.datetime.strptime(time_parts[-1] + am_or_pm, "%I:%M%p").time()
return start_time, end_time
def parse_schedule(content):
soup = bs4.BeautifulSoup(content, 'html.parser')
semester = soup.select_one("h2 strong").get_text(strip=True) # type: ignore
year = semester.split()[-1]
# schedule = {}
schedule = []
date = datetime.date(int(year), 1, 1)
earliest_date = datetime.date(int(year)+1, 1, 1)
for row in soup.select("tbody tr"):
date_label = row.find("th")
new_date = f"{date_label.get_text(strip=True)}, {year}" if date_label and date_label.get_text(strip=True) else None
if new_date:
new_date = new_date.replace(' ,', ',')
date = datetime.datetime.strptime(new_date, "%A, %B %d, %Y").date()
earliest_date = min(earliest_date, date)
cols = row.find_all("td")
if len(cols) == 3:
days = cols[0].get_text(strip=True)
class_time = cols[1].get_text(strip=True)
exam_time = cols[2].get_text(strip=True)
exam_datetime = datetime.datetime.strptime(f"{date} {exam_time.split(' - ')[0]}", "%Y-%m-%d %I:%M%p")
for day in parse_days(days):
start_time, end_time = parse_class_time(class_time)
# schedule[(day, start_time, end_time)] = exam_datetime
schedule.append({
"day": day,
"start_time": start_time.strftime("%H:%M"),
"end_time": end_time.strftime("%H:%M"),
"exam_datetime": exam_datetime.strftime("%Y-%m-%d %H:%M")
})
else: print("Unexpected number of columns in row:", len(cols))
return semester, schedule, earliest_date.strftime("%Y-%m-%d")
def download_schedule(url):
response = requests.get(url)
if response.status_code == 200:
return response.content
else:
raise Exception(f"Failed to retrieve schedule. Status code: {response.status_code}")
if __name__ == "__main__":
url = "https://www.snow.edu/offices/registrar/final_exams.html"
content = download_schedule(url)
# with open("final_exams.html", "r") as file:
# content = file.read()
semester, schedule, earliest_date = parse_schedule(content)
with open("www/final_exams.json", "w") as file:
json.dump({
"url": url,
"semester": semester,
"updated_date": datetime.date.today().strftime("%Y-%m-%d"),
"earliest_date": earliest_date,
"schedule": schedule,
}, file, indent=4)
# print("Semester:", semester)
# print("Schedule:")
# for (day, start_time, end_time), date in schedule.items():
# print(f"{day} {start_time} - {end_time}: {date}")

115
www/index.css Normal file
View File

@ -0,0 +1,115 @@
/*
Kyler Olsen
Feb 2025
*/
main {
padding-left: 20vw;
padding-right: 20vw;
}
h1, h2, h3, h4 {
font-family:
'Lucida Sans',
'Lucida Sans Regular',
'Lucida Grande',
'Lucida Sans Unicode',
Geneva,
Verdana,
sans-serif;
/* margin: 16px 64px; */
margin-top: 16px;
margin-bottom: 16px;
}
h1 {
text-align: center;
}
h1 .title {
font-size: 48px;
}
h1 .semester {
font-size: 24px;
font-style: italic;
}
table {
margin-left: auto;
margin-right: auto;
}
table, th, td {
border: 1px solid black;
}
th, td {
padding: 8px;
text-align: left;
}
hr {
margin-top: 16px;
}
p {
font-size: 18px;
/* text-indent: 4rem; */
/* margin: 16px 64px; */
margin-top: 16px;
margin-bottom: 16px;
}
ul {
display: block;
font-size: 18px;
/* margin: 16px 98px; */
margin-top: 16px;
margin-bottom: 16px;
}
ul ul {
margin-top: revert;
margin-bottom: revert;
}
a {
color: blue;
text-decoration: underline;
}
footer {
text-align: center;
padding: 8px;
padding-left: 20vw;
padding-right: 20vw;
}
@media screen and (max-width: 768px) {
main {
padding-left: 5vw;
padding-right: 5vw;
}
footer {
padding-left: 5vw;
padding-right: 5vw;
}
}
body[mode="dark"] {
background-color: #121212;
color: #dedede;
}
body[mode="dark"] table, body[mode="dark"] th, body[mode="dark"] td {
border: 1px solid #404040;
}
body[mode="dark"] a {
color: #3399ff;
}
.no-classes {
display: none;
}

78
www/index.html Normal file
View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Snow College Final Exams Finder</title>
<link rel="stylesheet" href="index.css">
<script src="index.js" defer></script>
</head>
<body>
<main>
<h1>
<div class="title">Snow College Final Exams Finder</div>
<div class="semester">Loading...</div>
</h1>
<p>
This tool helps you find the final exam schedule for Snow College.
It is not affiliated with Snow College. It was created by a Software
Engineering student at Snow and is provided as a convenience for
students. The schedule is based on the official Snow College final
exam schedule, but it may not be up to date or accurate. Always
check with your instructors or the official Snow College website for
the most current information.
<ul>
<li>
<a id="official-schedule" href="https://www.snow.edu/" target="_blank" rel="noopener noreferrer">
Official Snow College Website
</a>
</li>
</ul>
</p>
<div class="class-input">
<hr/>
<h2>Add Class</h2>
<label for="class-name">Class Name</label> <input type="text" id="class-name" placeholder="Enter Class Name">
<label for="Mon">M</label><input type="checkbox" id="Mon">
<label for="Tue">T</label><input type="checkbox" id="Tue">
<label for="Wed">W</label><input type="checkbox" id="Wed">
<label for="Thu">Th</label><input type="checkbox" id="Thu">
<label for="Fri">F</label><input type="checkbox" id="Fri">
<label for="time">Time</label> <input type="time" id="time">
<button id="add-class">Add Class</button>
</div>
<div class="class-list no-classes">
<hr/>
<h2>Class List</h2>
<table id="class-table">
<thead>
<tr>
<th>Class Name</th>
<th>Days</th>
<th>Time</th>
<th>Final Exam Date</th>
<th>Final Exam Time</th>
<th></th>
</tr>
</thead>
<tbody id="class-list">
<!-- Classes will be dynamically added here -->
</tbody>
</table>
</div>
</main>
<footer>
<div>
<hr/>
<small>
<strong>Not Affiliated With Snow College</strong><br/>
<span>&copy; 2025 Kyler Olsen</span>
<a href="/">Home</a>
<a id="dark">Dark Mode</a>
<a href="/contact.html">Contact</a>
<span>Last Updated June 2025</span>
</small>
</div>
</footer>
</body>
</html>

144
www/index.js Normal file
View File

@ -0,0 +1,144 @@
let examSchedule = {};
function dark_mode() {
if (document.querySelector("body").getAttribute("mode") == "dark") {
document.querySelector("body").setAttribute("mode", "light");
// localStorage.removeItem("projects_color_mode");
localStorage.setItem("projects_color_mode", "light");
} else {
document.querySelector("body").setAttribute("mode", "dark");
localStorage.setItem("projects_color_mode", "dark");
}
}
function get_exam_time(classDays, classTime) {
const toMinutes = t => {
const [h, m] = t.split(":").map(Number);
return h * 60 + m;
};
const [hours, minutes] = classTime.split(":").map(Number);
// One credit courses will test on the last day of the regularly scheduled class period.
if (classDays.length === 1) {
// Get the last date of the class given 'earliest_date' in examSchedule
const earliestDate = new Date(examSchedule.earliest_date);
const classDay = classDays[0];
let lastDate = new Date(earliestDate);
lastDate.setDate(lastDate.getDate() - 1);
let dayMap = { "M": 1, "T": 2, "W": 3, "Th": 4, "F": 5 };
while (lastDate.getDay() !== dayMap[classDay]) {
lastDate.setDate(lastDate.getDate() - 1);
}
// Set the time to the class time
lastDate.setHours(hours, minutes, 0, 0);
return lastDate;
}
// Classes that meet on the quarter or three quarter hour (i.e. 2:15 or 2:45)
// will need to check with the instructor for the finals schedule.
if (minutes === 15 || minutes === 45) { return null; }
// Normal classes will test according to the exam schedule.
for (let schedule of examSchedule.schedule) {
if (
schedule.day === classDays[0] &&
toMinutes(schedule.start_time) <= toMinutes(classTime) &&
toMinutes(schedule.end_time) >= toMinutes(classTime)
) {
return new Date(schedule.exam_datetime);
}
}
return null;
}
function remove_class(event) {
if (event.target.classList.contains("remove-class")) {
let row = event.target.closest("tr");
row.remove();
}
if (document.querySelector("#class-list").children.length === 0) {
document.querySelector(".class-list").classList.add("no-classes");
}
}
function add_class() {
if (document.querySelector(".class-list").classList.contains("no-classes")) {
document.querySelector(".class-list").classList.remove("no-classes");
}
let className = document.querySelector("#class-name").value.trim();
let classDaysReset = [];
let classDays = [];
if (document.querySelector("#Mon").checked) {classDaysReset.push("#Mon"); classDays.push("M");}
if (document.querySelector("#Tue").checked) {classDaysReset.push("#Tue"); classDays.push("T");}
if (document.querySelector("#Wed").checked) {classDaysReset.push("#Wed"); classDays.push("W");}
if (document.querySelector("#Thu").checked) {classDaysReset.push("#Thu"); classDays.push("Th");}
if (document.querySelector("#Fri").checked) {classDaysReset.push("#Fri"); classDays.push("F");}
let classTime = document.querySelector("#time").value.trim();
let examTime = get_exam_time(classDays, classTime);
let examTime_formatted = "";
if (examTime) {
examTime_formatted = examTime.toLocaleString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true
});
} else {
examTime_formatted = "Check with instructor for final schedule";
}
if (className && classDays.length > 0 && classTime) {
let newRow = document.createElement("tr");
newRow.innerHTML = `
<td>${className}</td>
<td>${classDays.join(", ")}</td>
<td>${classTime}</td>
<td colspan="2">${examTime_formatted}</td>
<td><button class="remove-class">Remove</button></td>
`;
document.querySelector("#class-list").appendChild(newRow);
document.querySelector("#class-name").value = "";
document.querySelector("#time").value = "";
classDaysReset.forEach(day => {
if (document.querySelector(day).checked) {
document.querySelector(day).checked = false;
}
});
newRow.querySelector(".remove-class").addEventListener("click", remove_class);
}
}
function schedule_loaded() {
document.querySelector('h1 .semester').innerText = examSchedule.semester;
document.querySelector('#official-schedule').href = examSchedule.url;
document.querySelector('#official-schedule').innerText = "Snow College Official Final Exam Schedule";
}
function main() {
document.querySelector("#dark").addEventListener("click", dark_mode);
document.querySelector("#add-class").addEventListener("click", add_class);
const savedMode = localStorage.getItem("projects_color_mode");
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedMode) {
document.querySelector("body").setAttribute("mode", savedMode);
} else if (isDarkMode) {
document.querySelector("body").setAttribute("mode", "dark");
}
fetch('./final_exams.json')
.then(response => response.json())
.then(data => {
examSchedule = data;
schedule_loaded();
})
.catch(error => {
console.error('Error fetching exam schedule:', error);
document.querySelector('h1 .semester').innerText = 'Error loading exam schedule';
});
}
document.addEventListener('DOMContentLoaded', main);