This script extracts your class schedule from a university portal (e.g., PeopleSoft/MyPortal) and generates an .ics
calendar file for importing into apps like Google Calendar, Outlook, or Apple Calendar.
- A browser with developer tools (Chrome, Firefox, Edge).
- Access to your My Weekly Schedule on the portal.
- Basic understanding of JavaScript (for troubleshooting).
- Navigate to:
MyPortal > My Record > My Weekly Schedule
. - Configure display settings:
- Show all 7 days (Monday–Sunday).
- Enable: "Show Class Title".
- Disable: "Show AM/PM" and "Show Instructors".
- Click "Refresh Calendar" after changing settings.
- Set the calendar to display the week starting on 27th January 2025 (or your desired start date).
- Open the browser’s developer tools:
- Press
F12
orCtrl+Shift+I
(Windows/Linux) /Cmd+Option+I
(Mac). - Go to the Console tab.
- Press
- Copy the script below and paste it into the console:
// vvv-- CODE --vvv
(async function (doc) {
let classes = [];
for (let i = 0; i < 14; i++) {
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const day_from_x = {};
const days = Array.from(doc.querySelectorAll('#WEEKLY_SCHED_HTMLAREA th'))
.slice(1)
.map((e, i) => {
let [day, month] = e.textContent.split('\n')[1].split(' ');
const x = Math.round(e.getBoundingClientRect().x);
day_from_x[x] = i;
return {
day: parseInt(day),
month: MONTHS.indexOf(month) + 1,
year: 2025,
};
});
const removeExtraSpaces = s => s.split(' ').filter(s => s.length > 0).join(' ');
const c = Array.from(doc.querySelectorAll('#WEEKLY_SCHED_HTMLAREA td > span'))
.map(e => e.parentNode)
.filter(e => day_from_x[Math.round(e.getBoundingClientRect().x)] !== undefined)
.map(e => {
const span = e.childNodes[0].childNodes;
const day = days[day_from_x[Math.round(e.getBoundingClientRect().x)]];
const formatCode = s => {
const words = removeExtraSpaces(s).split(' ');
return words[0] + words.slice(1).join(' ');
}
return {
name: removeExtraSpaces(`${formatCode(span[0].textContent)} ${span[2].textContent} ${span[4].textContent}`),
day,
time: span[6].textContent.split(' - ').map(s => s.split(':').join('')),
place: removeExtraSpaces(span[8].textContent),
}
});
console.log(`Found ${c.length} classes for Week ${i + 1}`);
classes.push(...c);
doc.getElementById('DERIVED_CLASS_S_SSR_NEXT_WEEK').click();
await new Promise(res => setTimeout(res, 2000));
}
return classes;
})(document.getElementById('ptifrmtgtframe').contentDocument).then(classes => {
const now = new Date();
const p = (n, l) => ("000" + n).slice(-l);
const f = (c, i) => `BEGIN:VEVENT
SUMMARY:${c.name}
DTSTAMP;TZID=Asia/Singapore:${p(now.getUTCFullYear(), 4)}${p(now.getMonth() + 1, 2)}${p(now.getUTCDate(), 2)}T000000
DTSTART;TZID=Asia/Singapore:${p(c.day.year, 4)}${p(c.day.month, 2)}${p(c.day.day, 2)}T${p(c.time[0], 4)}00
DTEND;TZID=Asia/Singapore:${p(c.day.year, 4)}${p(c.day.month, 2)}${p(c.day.day, 2)}T${p(c.time[1], 4)}00
LOCATION:${c.place}
END:VEVENT`;
const icsContent = `BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
${classes.map(f).join('\n')}
END:VCALENDAR
`;
let file = new Blob([icsContent], { type: 'text/calendar' });
const a = document.createElement("a");
const url = URL.createObjectURL(file);
a.href = url;
a.download = 'schedule.ics';
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
});