Skip to content

Commit 518f41d

Browse files
committed
2025-26 school year
1 parent 2aec4e4 commit 518f41d

File tree

7 files changed

+1582
-33
lines changed

7 files changed

+1582
-33
lines changed

client/src/components/schedule/DateSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export function Calendar(props: CalendarProps) {
8080
if (time) return; // Skip scroll behavior on timepicker; TODO: should we support this if we use non-range-constrained timepickers in the future?
8181

8282
// Set wrapper's scroll position to the offset of the current month, minus the day header and 1rem gap
83-
wrapper.current.scrollTop = currMonth.current.offsetTop - 48 - 16;
83+
if (today >= SCHOOL_START) wrapper.current.scrollTop = currMonth.current.offsetTop - 48 - 16;
8484
}, [wrapper, currMonth])
8585

8686
// Function to set the day without modifying the hour or minutes

client/src/pages/settings/Features.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default function Features() {
4444
await updateUserData('options.clock', value, auth, firestore);
4545
}
4646

47-
const years = [2025, 2026, 2027, 2028, 0];
47+
const years = [2026, 2027, 2028, 2029, 0];
4848

4949

5050
return (

scripts/deployAlternates.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import admin from 'firebase-admin';
22
import {readFileSync} from 'fs';
3-
import {info} from './util/logging';
3+
import {info, prompt} from './util/logging';
44

55

6-
admin.initializeApp({
7-
credential: admin.credential.cert('key.json')
8-
})
9-
106
;(async () => {
7+
const dev = await prompt('Type "prod" to deploy to production, anything else for dev.') !== "prod";
8+
console.log("deploying to", dev ? "dev" : "prod");
9+
10+
if(dev)
11+
process.env['FIRESTORE_EMULATOR_HOST'] = 'localhost:8080';
12+
13+
const config = dev ? {
14+
projectId: "gunnwatt"
15+
} : {
16+
credential: admin.credential.cert('key.json')
17+
}
18+
19+
admin.initializeApp(config)
1120
const alternates = JSON.parse(readFileSync('./output/alternates.json').toString());
1221
info(`Detected ${Object.keys(alternates).length} alternate schedules`)
1322

scripts/genAlternates.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import chalk from 'chalk';
77
import {error, info, warn} from './util/logging';
88
import schedule, {SCHOOL_START, SCHOOL_END_EXCLUSIVE, PeriodObj} from '@watt/shared/data/schedule';
99
import {numToWeekday} from '@watt/shared/util/schedule';
10+
import {DateTime} from 'luxon';
1011

1112

1213
// Constants
1314
const EARLIEST_AM_HOUR = 6;
1415

1516
const timeGetterRegex = /\(?(1?\d)(?::(\d{2}))? *(?:am)? *[-] *(1?\d)(?::(\d{2}))? *(noon|pm)?\)?/;
1617
const gradeGetterRegex = /(?<!Period |#)\d+(\/\d+)*(?!(?:st|nd|rd|th)\s+Period)/i;
17-
const altScheduleRegex = /staff pd|alt. sched|schedule|extended/i; // /schedule|extended|lunch/i
18+
const altScheduleRegex = /staff pd|alt. sched|schedule|extended|first day|finals/i; // /schedule|extended|lunch/i
1819
const noSchoolRegex = /holiday|no\s(students|school)|break|development/i;
1920
const primeReplacesSelfRegex = /PRIME (replaces|instead of) SELF|No SELF, extra PRIME/i;
2021
// const selfStudyHallRegex = /9\/10 (SELF|Study Hall), 11\/12 (SELF|Study Hall)/i;
@@ -84,6 +85,8 @@ function parseAlternate(summary: string | undefined, description: string | undef
8485

8586
// Parse away HTML tags, entities, and oddities
8687
description = description
88+
.replace(/(\):.+?\d([A-Za-z]))/g, '$1\n$2')
89+
.replace(/(\)|])([^\s:])/g, '$1\n$2')
8790
.replace(/\n\(/g, '(') // https://github.com/GunnWATT/watt/pull/73#discussion_r756519526
8891
.replace(/<\/?(p|div|br).*?>|\),? *(?=[A-Z\d])/g, '\n')
8992
.replace(/<.*?>/g, '') // Remove all html tags
@@ -113,19 +116,29 @@ function parseAlternate(summary: string | undefined, description: string | undef
113116
const endTime = eH * 60 + eM;
114117

115118
for (const raw of names) {
116-
const grades = raw.match(gradeGetterRegex)?.[0].split('/').map(grade => Number(grade));
117-
const name = raw.replace(gradeGetterRegex, '').trim();
119+
let grades = raw
120+
.match(gradeGetterRegex)?.[0]
121+
.split('/')
122+
.map(grade => Number(grade));
123+
124+
if (grades?.some(grade => grade < 9 || grade > 12)) {
125+
warn(`[${chalk.underline(date)}] Invalid grades: ${grades.join(', ')} in "${chalk.cyan(raw)}"`);
126+
grades = grades.filter(grade => grade >= 9 && grade <= 12);
127+
grades = grades.length ? grades : undefined;
128+
}
129+
130+
const name = raw.trim();
118131
if (!name) continue;
119132

120-
// Support both "Period 5" (standard) and "5th Period" (11/2/2022 schedule)
121-
const isNumberPeriod = name.match(/Period (\d)|(\d)(?:st|nd|rd|th) Period/i);
122-
const isStaffPrep = name.match(/Collaboration|Prep|Meetings?|Training|Mtgs|PLC/i);
133+
// Support both "Period 5" (standard) and "5th Period" (11/2/2022 schedule) and "(No) Zero Period" (8/21/2025 schedule)
134+
const isStaffPrep = name.match(/Collaboration|Prep|Meetings?|Training|Mtgs|PLC|Staff PD/i);
135+
const isNumberPeriod = name.match(/(?<!\bNo\s)Zero Period|Period (\d)|(\d)(?:st|nd|rd|th)(?: Period)?/i);
123136

124137
let fname = name;
125138
let newEndTime = endTime;
126139

127140
if (isNumberPeriod) {
128-
fname = isNumberPeriod[1] ?? isNumberPeriod[2];
141+
fname = isNumberPeriod[1] ?? isNumberPeriod[2] ?? '0';
129142
} else if (name.match(/Office Hours|Tutorial/i)) {
130143
fname = "O";
131144
warn(`[${chalk.underline(date)}] Parsed deprecated period Office Hours`);
@@ -207,34 +220,37 @@ function parseAlternate(summary: string | undefined, description: string | undef
207220
const prev: {[key: string]: PeriodObj[] | null} = JSON.parse(readFileSync('./output/alternates.json').toString());
208221

209222
// Fetch iCal source, parse
210-
const raw = await (await fetch('https://gunn.pausd.org/cf_calendar/feed.cfm?type=ical&feedID=3FC31A8EAE8A4918B2E582A69B519816')).text();
223+
const raw = await (await fetch('https://gunn.pausd.org/fs/calendar-manager/events.ics?calendar_ids[]=51')).text();
211224
const calendar = Object.values(ical.parseICS(raw));
212225

213226
const fAlternates: {[key: string]: PeriodObj[]} = {};
214-
let firstAlternate = new Date();
227+
let firstAlternate = DateTime.now();
215228

216229
// Populate `fAlternates` with unparsed day objects from iCal fetch
217230
for (const event of calendar) {
218-
const startDateObj = event.start!;
219-
const endDateObj = event.end;
231+
let startDateObj = DateTime.fromJSDate(event.start!).setZone('America/Los_Angeles');
232+
const endDateObj = DateTime.fromJSDate(event.end!).setZone('America/Los_Angeles');
233+
234+
// Invalid events - courtesy of the July 2025 Gunn website update...
235+
if (!startDateObj || !endDateObj) continue;
220236

221237
// If the alternate schedule does not lie within the school year, skip it
222-
if (startDateObj < SCHOOL_START.toJSDate() || startDateObj >= SCHOOL_END_EXCLUSIVE.toJSDate())
238+
if (startDateObj < SCHOOL_START || startDateObj >= SCHOOL_END_EXCLUSIVE)
223239
continue;
224240

225-
const schedule = parseAlternate(event.summary, event.description, startDateObj.toISOString().slice(0, 10))
241+
const schedule = parseAlternate(event.summary, event.description, startDateObj.toFormat('yyyy-MM-dd'))
226242
if (!schedule) continue;
227243

228244
// If an end date exists, add all dates between the start and end dates with the alternate schedule
229245
if (endDateObj) {
230-
while (startDateObj.toISOString().slice(5, 10) !== endDateObj.toISOString().slice(5, 10)) {
231-
fAlternates[startDateObj.toISOString().slice(5, 10)] = schedule;
232-
startDateObj.setUTCDate(startDateObj.getUTCDate() + 1);
246+
while (startDateObj.toFormat('yyyy-MM-dd') !== endDateObj.toFormat('yyyy-MM-dd')) {
247+
fAlternates[startDateObj.toFormat('MM-dd')] = schedule;
248+
startDateObj = startDateObj.plus({days: 1});
233249
}
234250
}
235251

236252
if (startDateObj < firstAlternate) firstAlternate = startDateObj;
237-
fAlternates[startDateObj.toISOString().slice(5, 10)] = schedule;
253+
fAlternates[startDateObj.minus({days: 1}).toFormat('MM-dd')] = schedule;
238254
}
239255

240256
const alternates: {[key: string]: PeriodObj[] | null} = {};
@@ -246,8 +262,8 @@ function parseAlternate(summary: string | undefined, description: string | undef
246262
let [month, day] = date.split('-').map(x => Number(x));
247263
if (month > 6) month -= 12; // Hackily account for our truncated ISO key format making 12-03 appear greater than 04-29
248264

249-
const firstMonth = firstAlternate.getMonth() + 1;
250-
if (month < firstMonth || (month === firstMonth && day < firstAlternate.getDate()))
265+
const firstMonth = firstAlternate.month + 1;
266+
if (month < firstMonth || (month === firstMonth && day < firstAlternate.day))
251267
alternates[date] = schedule;
252268
}
253269

Lines changed: 220 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,220 @@
1-
{}
1+
{
2+
"08-15": [
3+
{
4+
"n": "1",
5+
"s": 540,
6+
"e": 575
7+
},
8+
{
9+
"n": "2",
10+
"s": 585,
11+
"e": 620
12+
},
13+
{
14+
"n": "B",
15+
"s": 620,
16+
"e": 625
17+
},
18+
{
19+
"n": "3",
20+
"s": 635,
21+
"e": 670
22+
},
23+
{
24+
"n": "4",
25+
"s": 680,
26+
"e": 715
27+
},
28+
{
29+
"n": "L",
30+
"s": 715,
31+
"e": 770,
32+
"note": "Extended 10 minutes :))))"
33+
},
34+
{
35+
"n": "5",
36+
"s": 780,
37+
"e": 815
38+
},
39+
{
40+
"n": "6",
41+
"s": 825,
42+
"e": 860
43+
},
44+
{
45+
"n": "7",
46+
"s": 870,
47+
"e": 905
48+
}
49+
],
50+
"08-21": [
51+
{
52+
"n": "0",
53+
"s": 475,
54+
"e": 530
55+
},
56+
{
57+
"n": "1",
58+
"s": 540,
59+
"e": 615
60+
},
61+
{
62+
"n": "B",
63+
"s": 615,
64+
"e": 620
65+
},
66+
{
67+
"n": "Welcome Assembly",
68+
"s": 630,
69+
"e": 700,
70+
"note": "Block 1 of 2."
71+
},
72+
{
73+
"n": "Welcome Assembly",
74+
"s": 700,
75+
"e": 770,
76+
"note": "Block 2 of 2."
77+
},
78+
{
79+
"n": "L",
80+
"s": 770,
81+
"e": 800
82+
},
83+
{
84+
"n": "3",
85+
"s": 810,
86+
"e": 885
87+
},
88+
{
89+
"n": "4",
90+
"s": 895,
91+
"e": 970
92+
}
93+
],
94+
"08-22": [
95+
{
96+
"n": "5",
97+
"s": 540,
98+
"e": 615
99+
},
100+
{
101+
"n": "B",
102+
"s": 615,
103+
"e": 620
104+
},
105+
{
106+
"n": "6",
107+
"s": 630,
108+
"e": 705
109+
},
110+
{
111+
"n": "S",
112+
"s": 715,
113+
"e": 760,
114+
"grades": [
115+
9,
116+
10
117+
]
118+
},
119+
{
120+
"n": "L",
121+
"s": 760,
122+
"e": 790
123+
},
124+
{
125+
"n": "S",
126+
"s": 800,
127+
"e": 845,
128+
"grades": [
129+
11,
130+
12
131+
]
132+
},
133+
{
134+
"n": "7",
135+
"s": 855,
136+
"e": 930
137+
}
138+
],
139+
"08-28": [
140+
{
141+
"n": "0",
142+
"s": 475,
143+
"e": 530
144+
},
145+
{
146+
"n": "1",
147+
"s": 540,
148+
"e": 605
149+
},
150+
{
151+
"n": "B",
152+
"s": 605,
153+
"e": 620
154+
},
155+
{
156+
"n": "2",
157+
"s": 620,
158+
"e": 680
159+
},
160+
{
161+
"n": "iReady test",
162+
"s": 680,
163+
"e": 725,
164+
"note": "With your 2nd period class."
165+
},
166+
{
167+
"n": "L",
168+
"s": 725,
169+
"e": 755
170+
},
171+
{
172+
"n": "3",
173+
"s": 765,
174+
"e": 825
175+
},
176+
{
177+
"n": "Evacuation Drill",
178+
"s": 825,
179+
"e": 885
180+
},
181+
{
182+
"n": "4",
183+
"s": 895,
184+
"e": 955
185+
},
186+
{
187+
"n": "Back to School Night",
188+
"s": 1080,
189+
"e": 1260
190+
}
191+
],
192+
"08-29": [
193+
{
194+
"n": "5",
195+
"s": 540,
196+
"e": 600
197+
},
198+
{
199+
"n": "6",
200+
"s": 610,
201+
"e": 670
202+
},
203+
{
204+
"n": "B",
205+
"s": 670,
206+
"e": 675
207+
},
208+
{
209+
"n": "iReady test",
210+
"s": 685,
211+
"e": 730,
212+
"note": "With your 6th period class."
213+
},
214+
{
215+
"n": "7",
216+
"s": 740,
217+
"e": 800
218+
}
219+
]
220+
}

0 commit comments

Comments
 (0)