first commit
This commit is contained in:
commit
006e86da56
|
@ -0,0 +1,2 @@
|
|||
final_exams.html
|
||||
*.json
|
|
@ -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}")
|
|
@ -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;
|
||||
}
|
|
@ -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>© 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>
|
|
@ -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);
|
Loading…
Reference in New Issue