Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Beta Majors! #739

Merged
merged 11 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions packages/api/src/major/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
raw.*.html
tokens.*.json
125 changes: 125 additions & 0 deletions packages/api/src/major/major-collator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Major2 } from "@graduate/common";

const MAJORS: Record<string, Record<string, Major2>> = {};
const MAJOR_YEARS = new Set<string>();

const rootDir = "./src/major/majors";

interface YearData {
year: string;
}

interface YearCollegeData {
year: string;
college: string;
}

interface YearCollegeMajorData {
year: string;
college: string;
major: string;
}

async function fileExists(
fs: typeof import("fs/promises"),
path: string
): Promise<boolean> {
return await fs.access(path, fs.constants.F_OK).then(
() => true,
() => false
);
}

// TODO: this code is quick and dirty but works. this should be replaced with some dry-er code later.
/**
* Iterates over the ./majors directory, collecting majors and adding them to
* the exported MAJORS and MAJOR_YEARS object/set respectively. It prioritizes
* parsed.commit.json files over parsed.initial.json files because _.commit._
* files have been human-reviewed and _.initial._ files are raw scraper output.
*/
async function collateMajors() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My brain is slow but from what it looks like, we run through each year, each college and add all the majors. initial.json is our initial scrape and commit.json is the hand validated stuff?

If this is the case I can update this with comments/put it in our notion somewhere for knowledge transfer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call! Added some comments to the function.

// TODO: determine why these needed to be runtime imports (normal import statements didn't work here).
const fs = await import("fs/promises");
const path = await import("path");
const years = (
await fs.readdir(path.resolve(rootDir), {
withFileTypes: true,
})
)
.filter((dirent) => dirent.isDirectory())
.map(
(dirent): YearData => ({
year: dirent.name,
})
);

const colleges = (
await Promise.all(
years.map(async ({ year }) => {
const colleges = await fs.readdir(path.join(rootDir, year), {
withFileTypes: true,
});
return colleges
.filter((dirent) => dirent.isDirectory())
.map(
(college): YearCollegeData => ({
year: year,
college: college.name,
})
);
})
)
).flat();

const majors = (
await Promise.all(
colleges.map(async ({ year, college }) => {
const majors = await fs.readdir(path.join(rootDir, year, college), {
withFileTypes: true,
});
return majors
.filter((dirent) => dirent.isDirectory())
.map(
(major): YearCollegeMajorData => ({
year: year,
college: college,
major: major.name,
})
);
})
)
).flat();

years.forEach(({ year }) => {
MAJOR_YEARS.add(year);
MAJORS[year] = {};
});

const done = await Promise.all(
majors.map(async ({ year, college, major }) => {
const basePath = path.join(rootDir, year, college, major);
const commitFile = path.join(basePath, "parsed.commit.json");
const initialFile = path.join(basePath, "parsed.initial.json");

if (await fileExists(fs, commitFile)) {
const fileData = JSON.parse(
(await fs.readFile(commitFile)).toString()
) as Major2;
MAJORS[year][fileData.name] = fileData;
} else if (await fileExists(fs, initialFile)) {
const fileData = JSON.parse(
(await fs.readFile(initialFile)).toString()
) as Major2;
if (MAJORS[year]) MAJORS[year][fileData.name] = fileData;
}
})
);

console.log(
`Successfully loaded ${done.length} majors across ${MAJOR_YEARS.size} years!`
);
}

collateMajors();

export { MAJORS, MAJOR_YEARS };
13 changes: 6 additions & 7 deletions packages/api/src/major/major.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,36 @@ import {
} from "@graduate/common";
import { Injectable, Logger } from "@nestjs/common";
import { formatServiceCtx } from "../utils";
import { SUPPORTED_MAJOR_YEARS, SUPPORTED_MAJORS } from "./majors";
import { MAJOR_YEARS, MAJORS } from "./major-collator";

@Injectable()
export class MajorService {
private readonly logger: Logger = new Logger();

findByMajorAndYear(majorName: string, catalogYear: number): Major2 | null {
if (!SUPPORTED_MAJOR_YEARS.includes(catalogYear.toString())) {
if (!MAJOR_YEARS.has(String(catalogYear))) {
this.logger.debug(
{ mesage: "Major year not found", catalogYear },
MajorService.formatMajorServiceCtx("findByMajorAndYear")
);
return null;
}

const { majors, supportedMajorNames } = SUPPORTED_MAJORS[catalogYear];
if (!supportedMajorNames.includes(majorName)) {
if (!MAJORS[catalogYear][majorName]) {
this.logger.debug(
{ mesage: "Major within year not found", majorName, catalogYear },
MajorService.formatMajorServiceCtx("findByMajorAndYear")
);
return null;
}

return majors[majorName];
return MAJORS[catalogYear][majorName];
}

getSupportedMajors(): SupportedMajors {
const supportedMajors: SupportedMajors = {};
SUPPORTED_MAJOR_YEARS.forEach((year) => {
const { supportedMajorNames } = SUPPORTED_MAJORS[year];
MAJOR_YEARS.forEach((year) => {
const supportedMajorNames = Object.keys(MAJORS[year]);

const supportedMajorForYear: SupportedMajorsForYear = {};
supportedMajorNames.forEach((majorName) => {
Expand Down
Loading
Loading