A Personalized Daily Planner from Edlink
Goal
Generate a student's class schedule for "today" so we can help them take notes or make structured plans for the day.
Why
If you are building some sort of day planner feature, or you want to be able to show upcoming work, you'll need to know a student's schedule for the day. Since schools often have changing bell schedules, and alternating class days, it can be more complicated than you might expect to generate an accurate schedule. Fortunately, Edlink has got your back!
How
Here's an example JavaScript script that uses the Edlink API to generate a student's daily class schedule, including class names, period names, and times.
This script assumes you have:
- A Graph API access token and a valid Student ID.
- Node.js installed to run the script.
Save the following to planner.js
.
// IMPORTANT: Replace the following variables with valid information for your system and use case.
const GRAPH_ACCESS_TOKEN = 'YOUR_GRAPH_ACCESS_TOKEN';
const TARGET_DATE = '2023-09-01'; // The day we are generating the schedule for (YYYY-MM-DD format)
const STUDENT_ID = '00000000-0000-0000-0000-000000000000'; // the userId of the student whose schedule you are trying to generate
// helper function for api calls
const fetchWithToken = async (url, token) => {
const headers = {
Authorization: `Bearer ${token}`
};
const response = await fetch(url, { headers });
if (!response.ok) {
console.error(url, response);
throw new Error(`HTTP error! Status: ${response.status}`);
}
return await response.json();
};
// helper function for iterating through all the pages in a list of api calls
const getAllPaginated = async (url, token) => {
let list = [];
while (url) {
process.stdout.write('.');
const data = await fetchWithToken(url, token);
list.push(...data.$data);
url = data.$next; // Get the URL for the next page, or null if no more pages
}
return list;
};
async function getStudentDayPlan(graph_token, user_id, target_date_string, student_time_zone = 'America/New_York') {
const API_BASE_URL = 'http://localhost:9900/api/v2/graph';
const TIME_OPTIONS = { hour: 'numeric', minute: 'numeric', hour12: true, timeZone: student_time_zone };
try {
// 1. Get all classes in the system
const student_classes = await getAllPaginated(`${API_BASE_URL}/people/${user_id}/classes?$first=1000`, graph_token);
// 2. For each class, fetch its meetings and filter for the target date.
const relevant_meetings = [];
const unique_period_ids = new Set();
for (const cls of student_classes) {
// important to include $expand=day
const meetings = await getAllPaginated(`${API_BASE_URL}/classes/${cls.id}/meetings?$expand=day`, graph_token);
for (const meeting of meetings) {
// Compare only the date part of the meeting's ISO string
const meeting_date = new Date(meeting.day.date).toISOString().split('T')[0];
if (meeting_date === target_date_string) {
if (meeting.type === 'period' && meeting.period_id) {
unique_period_ids.add(meeting.period_id);
}
relevant_meetings.push({
class_id: cls.id,
class_name: cls.name,
meeting_id: meeting.id,
period_id: meeting.period_id,
meeting_type: meeting.type
});
}
}
}
if (relevant_meetings.length === 0 || unique_period_ids.size === 0) {
return relevant_meetings;
} else {
console.log(`Found ${relevant_meetings.length} relevant meetings on ${target_date_string}.`);
}
// 3. Fetch details for all unique periods identified.
const periods_map = new Map();
for (const period_id of unique_period_ids) {
const period = (await fetchWithToken(`${API_BASE_URL}/periods/${period_id}`, graph_token)).$data;
periods_map.set(period_id, period);
}
// 4. Assemble the final schedule with formatted times.
const final_schedule = relevant_meetings
.map((entry) => {
const period = periods_map.get(entry.period_id);
const cls = student_classes.find((c) => c.id === entry.class_id);
if (period && cls) {
// Combine the target date with the period's time for accurate Date objects
// The date part of period.start_time/end_time might be arbitrary, so we use target_date_string
const start_time_combined = `${target_date_string}T${new Date(period.start_time).toISOString().split('T')[1]}`;
const end_time_combined = `${target_date_string}T${new Date(period.end_time).toISOString().split('T')[1]}`;
const start_time = new Date(start_time_combined);
const end_time = new Date(end_time_combined);
// Format times for display in the student's local timezone
const formatted_start_time = start_time.toLocaleTimeString('en-US', TIME_OPTIONS);
const formatted_end_time = end_time.toLocaleTimeString('en-US', TIME_OPTIONS);
return {
class_name: cls.name,
period_name: period.name,
start_time: formatted_start_time,
end_time: formatted_end_time,
type: entry.meeting_type
};
} else if (entry.meeting_type === 'day' && cls) {
return {
class_name: cls.name,
period_name: 'Full Day',
start_time: 'All Day',
end_time: 'All Day',
type: entry.meeting_type
};
}
return null; // Filter out entries that couldn't be fully resolved
})
.filter(Boolean); // Remove any null entries
// 5. Sort the schedule by start time.
final_schedule.sort((a, b) => {
// Handle 'All Day' entries first
if (a.start_time === 'All Day' && b.start_time !== 'All Day') return -1;
if (a.start_time !== 'All Day' && b.start_time === 'All Day') return 1;
if (a.start_time === 'All Day' && b.start_time === 'All Day') return 0;
// For actual times, parse and compare using a dummy date to ensure only time is compared
const time_a = new Date(`${target_date_string} ${a.start_time}`);
const time_b = new Date(`${target_date_string} ${b.start_time}`);
return time_a.getTime() - time_b.getTime();
});
return final_schedule;
} catch (error) {
console.error('Error generating student day plan:');
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error('Data:', error.response.data);
console.error('Status:', error.response.status);
console.error('Headers:', error.response.headers);
} else if (error.request) {
// The request was made but no response was received
console.error('No response received:', error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.error('Error message:', error.message);
}
return null;
}
}
// --- Usage Example ---
if (!GRAPH_ACCESS_TOKEN || GRAPH_ACCESS_TOKEN === 'YOUR_GRAPH_ACCESS_TOKEN') {
console.error("ERROR: Please replace 'YOUR_GRAPH_ACCESS_TOKEN' with a valid access token obtained from Edlink SSO.");
} else {
getStudentDayPlan(GRAPH_ACCESS_TOKEN, STUDENT_ID, TARGET_DATE).then((schedule) => {
if (schedule) {
console.log('\n--- Final Student Day Plan ---');
if (schedule.length === 0) {
console.log(`No classes found for ${TARGET_DATE}.`);
} else {
schedule.forEach((item) => {
console.log(`${item.start_time} - ${item.end_time}: ${item.class_name} (${item.period_name})`);
});
}
} else {
console.log('Failed to generate schedule due to an error.');
}
});
}
Running node planner.js
should produce something like the following:
...........Found 5 relevant meetings on 2023-09-01.
--- Final Student Day Plan ---
8:00 AM - 10:00 AM: Homeroom (Period 1)
10:00 AM - 12:00 PM: Reading (Period 2)
12:00 PM - 1:00 PM: Lunch (Period 3)
1:00 PM - 2:00 PM: Geography (Period 4)
2:00 PM - 2:30 PM: Pre-Calculus (Period 5)