Skip to content

Commit a706e87

Browse files
GostmeaperTenType
andauthored
feat: Course Syllabi Lookup (#89)
* feat: course syllabi json * feat: basic functionality * fix: rm unused declaration * fix: sort by year and season * fix: remove async functionality * feat: add note and rm instructor if not found * fix: 4 digit and 2 digit year value allowed * fix: tentype issues * fix: clarify error message when inputting multiple/invalid course codes --------- Co-authored-by: TenType <55125103+TenType@users.noreply.github.com>
1 parent 8ee95c1 commit a706e87

File tree

5 files changed

+197944
-224541
lines changed

5 files changed

+197944
-224541
lines changed

src/commands/courses.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
underline,
1515
} from "discord.js";
1616
import { FYW_MINIS, SCOTTYLABS_URL } from "../constants.js";
17+
import SyllabiData from "../data/course-api.syllabi.json" with { type: "json" };
1718
import CoursesData from "../data/finalCourseJSON.json" with { type: "json" };
1819
import { parseAndEvaluate } from "../modules/operator-parser.ts";
1920
import type { SlashCommand } from "../types.d.ts";
@@ -44,6 +45,17 @@ type FCERecord = {
4445
responseRate: number;
4546
};
4647

48+
type Syllabus = {
49+
_id: {
50+
$oid: string;
51+
};
52+
season: string;
53+
year: number;
54+
number: string;
55+
section: string;
56+
url: string;
57+
};
58+
4759
function loadCoursesData(): Record<string, Course> {
4860
return CoursesData as Record<string, Course>;
4961
}
@@ -167,6 +179,40 @@ function loadFCEData(): Record<string, FCEData> {
167179
return fceMap;
168180
}
169181

182+
function loadSyllabiData(): Record<string, Syllabus[]> {
183+
const syllabiData = SyllabiData as Syllabus[];
184+
const syllabi: Record<string, Syllabus[]> = {};
185+
186+
const seasonOrder: Record<string, number> = {
187+
F: 0,
188+
S: 1,
189+
M: 2,
190+
N: 3,
191+
};
192+
193+
for (const entry of syllabiData) {
194+
const courseid = formatCourseNumber(entry.number) ?? "";
195+
if (!syllabi[courseid]) {
196+
syllabi[courseid] = [];
197+
}
198+
syllabi[courseid].push(entry);
199+
}
200+
201+
for (const courseid in syllabi) {
202+
syllabi[courseid]!.sort((a, b) => {
203+
if (a.year !== b.year) {
204+
return b.year - a.year;
205+
}
206+
207+
return (
208+
(seasonOrder[a.season] ?? 99) - (seasonOrder[b.season] ?? 99)
209+
);
210+
});
211+
}
212+
213+
return syllabi;
214+
}
215+
170216
const command: SlashCommand = {
171217
data: new SlashCommandBuilder()
172218
.setName("courses")
@@ -209,6 +255,19 @@ const command: SlashCommand = {
209255
)
210256
.setRequired(true),
211257
),
258+
)
259+
.addSubcommand((subcommand) =>
260+
subcommand
261+
.setName("syllabus")
262+
.setDescription("Get pdfs of syllabi for a course")
263+
.addStringOption((option) =>
264+
option
265+
.setName("course_id")
266+
.setDescription(
267+
"Course code in XX-XXX or XXXXX format (e.g., 15-112)",
268+
)
269+
.setRequired(true),
270+
),
212271
),
213272
async execute(interaction) {
214273
const coursesData = loadCoursesData();
@@ -581,6 +640,99 @@ const command: SlashCommand = {
581640
return interaction.reply({ embeds: [embed] });
582641
}
583642
}
643+
if (interaction.options.getSubcommand() === "syllabus") {
644+
const convertSem: Record<string, string> = {
645+
F: "Fall",
646+
S: "Spring",
647+
M: "Summer",
648+
N: "Summer",
649+
};
650+
651+
const syllabi = loadSyllabiData();
652+
const fceData = loadFCEData();
653+
654+
const courseid = formatCourseNumber(
655+
interaction.options.getString("course_id", true),
656+
);
657+
658+
if (!courseid) {
659+
return interaction.reply({
660+
content:
661+
"Please provide a valid course code in the format XX-XXX or XXXXX.",
662+
flags: MessageFlags.Ephemeral,
663+
});
664+
}
665+
666+
const course = coursesData[courseid];
667+
668+
if (!syllabi[courseid] || !course) {
669+
return interaction.reply({
670+
content: `Course ${courseid} not found.`,
671+
flags: MessageFlags.Ephemeral,
672+
});
673+
}
674+
let foundSyllabi = syllabi[courseid];
675+
676+
const uniqueSyllabi = [
677+
...new Map(foundSyllabi.map((s) => [s.url, s])).values(),
678+
];
679+
680+
const embeds: EmbedBuilder[] = [];
681+
let currentDesc = "";
682+
683+
let links = 0;
684+
685+
for (const syllabus of uniqueSyllabi) {
686+
/*
687+
Currently this uses fce data to find instructor data
688+
However, syllabi data should be updated to include instructor,
689+
and courses data should be changed to correctly include
690+
syllabi and session as intended
691+
*/
692+
693+
let fceRec = fceData[courseid]?.records ?? [];
694+
let fceEntry = undefined;
695+
696+
for (const rec of fceRec) {
697+
// Rec.year and Syllabus.year will both be 2 digits
698+
if (
699+
rec.semester == convertSem[syllabus.season] &&
700+
rec.year == syllabus.year + 2000 &&
701+
rec.section == syllabus.section
702+
) {
703+
fceEntry = rec;
704+
}
705+
}
706+
const line: string = hyperlink(
707+
`${syllabus.season}${syllabus.year}: ${syllabus.number}-${syllabus.section} ${fceEntry?.instructor ? `(${fceEntry?.instructor})` : ""} \n`,
708+
`${syllabus.url}`,
709+
);
710+
if (links >= 20) {
711+
embeds.push(
712+
new EmbedBuilder()
713+
.setTitle(`Syllabi for ${courseid}: ${course.name}`)
714+
.setURL(`${SCOTTYLABS_URL}/course/${courseid}`)
715+
.setDescription(currentDesc),
716+
);
717+
currentDesc = line;
718+
links = 1;
719+
} else {
720+
currentDesc += line;
721+
links++;
722+
}
723+
}
724+
725+
if (currentDesc) {
726+
embeds.push(
727+
new EmbedBuilder()
728+
.setTitle(`Syllabi for ${courseid}: ${course.name}`)
729+
.setURL(`${SCOTTYLABS_URL}/course/${courseid}`)
730+
.setDescription(currentDesc),
731+
);
732+
}
733+
734+
return new EmbedPaginator(embeds).send(interaction);
735+
}
584736
},
585737
};
586738

0 commit comments

Comments
 (0)