Skip to content

Commit

Permalink
Implement get_overview
Browse files Browse the repository at this point in the history
Also reformat some parts of scrape.ts and types.ts, and create a new
file types-shared.ts to house type definitions that are shared between
scrape.ts and other files.
  • Loading branch information
psvenk committed Dec 13, 2020
1 parent 3585a8d commit 9a91991
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 46 deletions.
130 changes: 114 additions & 16 deletions src/scrape.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import fetch from "node-fetch";
import { URLSearchParams } from "url";

import type { Session, PDFFileInfo, PDFFile, ClassInfo } from "./types";
import type {
Session,
PDFFileInfo,
ClassInfo,
ClassDetails,
} from "./types";
import type { PDFFile, OverviewItem } from "./types-shared";
// Using `import type` with an enum disallows accessing the enum variants
import { Quarter } from "./types";
import { Quarter } from "./types-shared";

async function get_student(
export async function get_student(
username: string, password: string, quarter: Quarter
) {
return await get_session(username, password, async session => {
const { student_name, student_oid } = await get_student_info(session);
const quarter_oids = await get_quarter_oids(session);
return await get_academics(session, student_oid, quarter_oids);
const academics = await get_academics(session, student_oid, quarter_oids);
const class_details = await Promise.all(academics.map(async class_info =>
get_class_details(session, class_info)));
const overview = get_overview(class_details);
return overview;
});
}

Expand Down Expand Up @@ -54,14 +64,14 @@ async function get_quarter_oids(
}

/**
* Get basic information (name, term grades, and OID) about classes
* Get basic information (name, grades, teacher, term, and OID) about classes
*/
async function get_academics(
{ session_id }: Session, student_oid: string,
quarter_oids: Map<Quarter, string>
): Promise<ClassInfo[]> {
const get_classes = async (quarter_oid: string) => await (await fetch(
"https://aspen.cpsd.us/aspen/rest/lists/academics.classes.list?" +
"https://aspen.cpsd.us/aspen/rest/lists/academics.classes.list?fieldSetOid=fsnX2Cls++++++&" +
new URLSearchParams({
"selectedStudent": student_oid,
"customParams": `selectedYear|current;selectedTerm|${quarter_oid}`,
Expand Down Expand Up @@ -91,7 +101,12 @@ async function get_academics(
}

// For each class, assemble a ClassInfo object
return all_classes.map(({ oid, relSscMstOid_mstDescription: name }) => {
return all_classes.map(({
oid,
relSscMstOid_mstDescription: name,
relSscMstOid_mstStaffView: teachers,
sscTermView: term,
}) => {
// Mapping the terms in which this class meets to the corresponding term
// averages
const grades = new Map<Quarter, string>();
Expand All @@ -109,14 +124,97 @@ async function get_academics(
// Enter the grade for this term into the grades mapping
grades.set(quarter, (term_data.cfTermAverage ?? "") as string);
}
return { name, oid, grades };

let teacher = "";
try {
[{ name: teacher }] = teachers;
} catch (e) {
// In the case of a TypeError (if the class has no teachers),
// let teacher be ""
if (!(e instanceof TypeError)) {
throw e;
}
}

return { name, grades, teacher, term, oid };
});
}

/**
* Get extended information about a class (attendance, categories)
*/
async function get_class_details(
{ session_id }: Session, class_info: ClassInfo
): Promise<ClassDetails> {
const { averageSummary, attendanceSummary } = await (await fetch(
`https://aspen.cpsd.us/aspen/rest/studentSchedule/${class_info.oid}/academics`, {
headers: {
"Cookie": `JSESSIONID=${session_id}`,
},
}
)).json();

const attendance = { absent: 0, tardy: 0, dismissed: 0 };
for (const { total, type } of attendanceSummary) {
switch (type) {
case "Absent": attendance.absent = total; break;
case "Tardy": attendance.tardy = total; break;
case "Dismissed": attendance.dismissed = total; break;
}
}
const categories: { [key: string]: string } = {};
for (const {
category, percentageQ1, percentageQ2, percentageQ3, percentageQ4
} of averageSummary) {
if (category !== "Gradebook average") {
categories[category] = (parseFloat(percentageQ1 || percentageQ2 ||
percentageQ3 || percentageQ4) / 100.0).toString();
}
}

return { attendance, categories, ...class_info };
}

function get_overview(class_details: ClassDetails[]): OverviewItem[] {
return class_details.map(({
name,
grades,
teacher,
term,
oid,
attendance: { absent, tardy, dismissed },
}) => {
const [q1, q2, q3, q4] =
[Quarter.Q1, Quarter.Q2, Quarter.Q3, Quarter.Q4].map(q =>
parseFloat(grades.get(q) ?? ""));
// Get all quarter grades that are not NaN, and average them to get the
// year-to-date grade
const quarter_grades = [q1, q2, q3, q4].filter(x => !isNaN(x));
const ytd = quarter_grades.length ?
quarter_grades.reduce((a, b) => a + b) : NaN;
// Custom function for formatting numbers so that NaN is mapped to the
// empty string
const format = (x: number) => isNaN(x) ? "" : x.toString();
return {
class: name,
teacher: teacher,
term: term,
q1: format(q1),
q2: format(q2),
q3: format(q3),
q4: format(q4),
ytd: format(ytd),
absent: format(absent),
tardy: format(tardy),
dismissed: format(dismissed),
};
});
}

/**
* Return an array containing all PDF files
*/
async function get_pdf_files(
export async function get_pdf_files(
username: string, password: string
): Promise<PDFFile[]> {
return await get_session(username, password, async session => {
Expand Down Expand Up @@ -214,12 +312,12 @@ async function get_session<T>(
if (require.main === module) {
const [username, password] = process.argv.slice(2);
get_student(username, password, 1).then(
console.log,
e => console.error(`Error: ${e.message}`)
console.log, e => {
if (e.message === "Invalid login") {
console.error(`Error: ${e.message}`);
} else {
console.error(e);
}
}
);
}

module.exports = {
get_student,
get_pdf_files,
};
85 changes: 85 additions & 0 deletions src/types-shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
export interface StudentData {
classes: Class[];
recent: Recent;
overview?: OverviewItem[];
username: string;
quarter: Quarter;
}

export interface Class {
name: string;
grade: string;
// Maps category names to decimals (stored as strings)
categories: { [key: string]: string };
assignments: Assignment[];
}

export interface Assignment {
name: string;
category: string;
date_assigned: string;
date_due: string;
feedback: string;
assignment_id: string;
special: string;
score: number;
max_score: number;
}

export interface OverviewItem {
class: string;
teacher: string;
term: string;
q1: string;
q2: string;
q3: string;
q4: string;
ytd: string;
absent: string;
tardy: string;
dismissed: string;
}

export interface Recent {
// Empty array at the moment because Aspen does not show recent activity
// w.r.t. assignments
recentActivityArray: [];
recentAttendanceArray: AttendanceEvent[];
}

export interface AttendanceEvent {
date: string;
period: string;
code: string;
classname: string;
dismissed: string;
absent: string;
excused: string;
tardy: string;
}

export interface Schedule {
black: ScheduleItem[];
silver: ScheduleItem[];
}

export interface ScheduleItem {
id: string;
name: string;
teacher: string;
room: string;
aspenPeriod: string;
}

export interface PDFFile {
title: string;
content: string;
}

export enum Quarter {
Current = 0,
Q1,
Q2,
Q3,
Q4,
}
41 changes: 11 additions & 30 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,29 @@
// Internal to scrape.ts
import { Quarter } from "./types-shared";

export interface Session {
session_id: string;
apache_token: string;
}

// Internal to scrape.ts
export interface PDFFileInfo {
id: string;
filename: string;
}

// Used by client code
export interface PDFFile {
title: string;
content: string;
}

// Internal to scrape.ts
export interface ClassInfo {
name: string;
grades: Map<Quarter, string>;
oid: string;
}

// Used by client code
export interface OverviewItem {
class: string;
teacher: string;
term: string;
q1: string;
q2: string;
q3: string;
q4: string;
ytd: string;
absent: string;
tardy: string;
dismissed: string;
oid: string;
}

// Used by client code
export enum Quarter {
Current = 0,
Q1,
Q2,
Q3,
Q4,
export interface ClassDetails extends ClassInfo {
attendance: {
absent: number;
tardy: number;
dismissed: number;
};
// Maps category names to decimals (stored as strings)
categories: { [key: string]: string };
}

0 comments on commit 9a91991

Please sign in to comment.