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

Requirement Migrations #874

Merged
merged 9 commits into from
Apr 20, 2024
3 changes: 2 additions & 1 deletion src/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import csRequirements, { csAdvisors } from './majors/cs';
import deaRequirements, { deaAdvisors } from './majors/dea';
import easRequirements, { easAdvisors } from './majors/eas';
import economicsRequirements, { economicsAdvisors } from './majors/econ';
import eceRequirements, { eceAdvisors } from './majors/ece';
import eceRequirements, { eceAdvisors, eceMigrations } from './majors/ece';
import essRequirements, { essAdvisors } from './majors/ess';
import englishRequirements, { englishAdvisors } from './majors/engl';
import envEngineeringRequirements, { envEngineeringAdvisors } from './majors/envE';
Expand Down Expand Up @@ -256,6 +256,7 @@ const json: RequirementsJson = {
schools: ['EN'],
requirements: eceRequirements,
advisors: eceAdvisors,
migrations: eceMigrations,
abbrev: 'ECE',
},
ENGL: {
Expand Down
28 changes: 23 additions & 5 deletions src/data/majors/ece.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Course, CollegeOrMajorRequirement } from '../../requirements/types';
import { Course, CollegeOrMajorRequirement, RequirementMigration } from '../../requirements/types';
import {
ifCodeMatch,
includesWithSubRequirements,
Expand Down Expand Up @@ -33,13 +33,13 @@ const eceRequirements: readonly CollegeOrMajorRequirement[] = [
},
{
name: 'Core Courses',
description: 'ECE 2100, ECE 2200 ECE 3400',
description: 'ECE 2100, ECE 2720',
source:
'https://www.ece.cornell.edu/ece/programs/undergraduate-programs/majors/program-requirements',
checker: includesWithSubRequirements(['ECE 2100'], ['ECE 2200'], ['ECE 3400']),
checker: includesWithSubRequirements(['ECE 2100'], ['ECE 2720']),
fulfilledBy: 'courses',
perSlotMinCount: [1, 1, 1],
slotNames: ['ECE 2100', 'ECE 2200', 'ECE 3400'],
perSlotMinCount: [1, 1],
slotNames: ['ECE 2100', 'ECE 2720'],
},
{
name: 'Foundation Courses',
Expand Down Expand Up @@ -175,3 +175,21 @@ export default eceRequirements;
export const eceAdvisors: AdvisorGroup = {
advisors: [{ name: 'Sharif Ewais-Orozco', email: 'ugrad-coordinator@ece.cornell.edu' }],
};

export const eceMigrations: RequirementMigration[] = [
{
entryYear: 2020, // For students with an entry year of 2020 or earlier, this requirement applies. For students with an entry year of 2021 or later, the above Core Courses requirement applies
type: 'Modify',
fieldName: 'Core Courses',
newValue: {
name: 'Core Courses',
description: 'ECE 2100, ECE 2200, & ECE 3400',
source:
'https://www.ece.cornell.edu/ece/programs/undergraduate-programs/majors/program-requirements',
checker: includesWithSubRequirements(['ECE 2100'], ['ECE 2200'], ['ECE 3400']),
fulfilledBy: 'courses',
perSlotMinCount: [1, 1, 1],
slotNames: ['ECE 2100', 'ECE 2200', 'ECE 3400'],
},
},
];
4 changes: 4 additions & 0 deletions src/requirement-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ type DecoratedCollegeOrMajorRequirement = RequirementCommon &
readonly conditions?: Readonly<RequirementCourseConditions>;
}>;

type MigrationWithDecoratedRequirement = RequirementMigration & {
newValue?: DecoratedCollegeOrMajorRequirement;
};

/**
* CourseTaken is the data type used in requirement computation.
* It's a significantly simplified version of FirestoreSemesterCourse to make it easy to mock for
Expand Down
45 changes: 37 additions & 8 deletions src/requirements/decorated-requirements.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

86 changes: 74 additions & 12 deletions src/requirements/requirement-frontend-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ export function allowCourseDoubleCountingBetweenRequirements(
requirementA: RequirementWithIDSourceType,
requirementB: RequirementWithIDSourceType
): boolean {
// console.log("Entered allow course double counting thing")
if (!requirementA || !requirementB) {
// If requirement is undefined, handle accordingly
return false; // Or any other value, or skip this item
}
// console.log(requirementA)
// console.log(requirementB)
nidhi-mylavarapu marked this conversation as resolved.
Show resolved Hide resolved
const allowCourseDoubleCounting =
requirementA.allowCourseDoubleCounting || requirementB.allowCourseDoubleCounting || false;

Expand Down Expand Up @@ -118,27 +125,79 @@ export function allowCourseDoubleCountingBetweenRequirements(
return false;
}

const reqWithSourceInfo = (
req: DecoratedCollegeOrMajorRequirement,
sourceType: 'Major' | 'Minor',
sourceSpecificName: string
) => ({
...req,
id: `${sourceType}-${sourceSpecificName}-${req.name}`,
sourceType,
sourceSpecificName,
});

/**
* Get the requirements for a provided collection of majors/minors
*
* @param sourceType The type of the field of study, e.g. 'Major' or 'Minor'
* @param fields the names of the majors/minors
* @returns An array of requirements corresponding to every field of study in `fields`
*/
const fieldOfStudyReqs = (sourceType: 'Major' | 'Minor', fields: readonly string[]) => {
const fieldOfStudyReqs = (
sourceType: 'Major' | 'Minor',
fields: readonly string[],
entryYear: string
) => {
const jsonKey = sourceType.toLowerCase() as 'major' | 'minor';
const fieldRequirements = requirementJson[jsonKey];

return fields
.map(field => {
const fieldRequirement = fieldRequirements[field];
return fieldRequirement?.requirements.map(
it =>
(({
...it,
id: `${sourceType}-${field}-${it.name}`,
sourceType,
sourceSpecificName: field,
} as const) ?? [])
const requirementMigrations = fieldRequirement?.migrations || [];

// Filter migrations based on entryYear
const filteredMigrations = requirementMigrations.filter(
migration => parseInt(entryYear, 10) <= migration.entryYear
);

// Collect all requirements corresponding to 'Add' migrations in one array
const addMigrationNewValues = filteredMigrations
.filter(migration => migration.type === 'Add')
.map(migration => migration.newValue);

return (
fieldRequirement?.requirements
// Find migrations that match existing requirements - must be 'Delete' or 'Modify'
.filter(it => {
const matchingMigration = filteredMigrations.find(
migration => migration.fieldName === it.name
);

// If a reqiurement matches a 'Delete' migration, return false (filter it out of overall requirements)
if (matchingMigration) {
if (matchingMigration.type === 'Delete') {
return false;
}
}
return true;
})
.map(it => {
const matchingMigration = filteredMigrations.find(
migration => migration.fieldName === it.name
);

// If a requirement matches a 'Modify' migration, map it to it's new value
if (matchingMigration) {
if (matchingMigration.type === 'Modify') {
return matchingMigration.newValue as DecoratedCollegeOrMajorRequirement;
}
}
return it;
})
.concat(addMigrationNewValues.filter(Boolean) as DecoratedCollegeOrMajorRequirement[])
// Use helper to map requirements to requirements with source info
.map(it => reqWithSourceInfo(it, sourceType, field))
);
})
.flat();
Expand Down Expand Up @@ -181,6 +240,7 @@ export function getUserRequirements({
college,
major: majors,
minor: minors,
entranceYear,
grad,
}: AppOnboardingData): readonly RequirementWithIDSourceType[] {
// check university & college & major & minor requirements
Expand All @@ -201,8 +261,8 @@ export function getUserRequirements({
)
: [];
const collegeReqs = college ? specializedForCollege(college, majors) : [];
const majorReqs = fieldOfStudyReqs('Major', majors);
const minorReqs = fieldOfStudyReqs('Minor', minors);
const majorReqs = fieldOfStudyReqs('Major', majors, entranceYear);
const minorReqs = fieldOfStudyReqs('Minor', minors, entranceYear);
const gradReqs = grad
? requirementJson.grad[grad].requirements.map(
it =>
Expand All @@ -215,7 +275,9 @@ export function getUserRequirements({
)
: [];
// flatten all requirements into single array
return [uniReqs, collegeReqs, majorReqs, minorReqs, gradReqs].flat();
return [uniReqs, collegeReqs, majorReqs, minorReqs, ...gradReqs]
.flat()
.filter(Boolean) as RequirementWithIDSourceType[];
}

/**
Expand Down
34 changes: 32 additions & 2 deletions src/requirements/requirement-json-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
RequirementChecker,
Course,
MutableMajorRequirements,
RequirementMigration,
} from './types';
import sourceRequirements, { colleges } from '../data';
import { NO_FULFILLMENTS_COURSE_ID, SPECIAL_COURSES } from '../data/constants';
Expand Down Expand Up @@ -36,6 +37,9 @@ type InitialRequirementDecorator = (
type RequirementDecorator = (
requirement: DecoratedCollegeOrMajorRequirement
) => DecoratedCollegeOrMajorRequirement;
type MigrationRequirementDecorator = (
migration: RequirementMigration
) => MigrationWithDecoratedRequirement;

const getEligibleCoursesFromRequirementCheckers = (
checkers: readonly RequirementChecker[]
Expand Down Expand Up @@ -294,6 +298,22 @@ const sortRequirementCourses: RequirementDecorator = requirement => {
}
};

const decorateMigrationValue: MigrationRequirementDecorator = migration => {
if (migration.newValue) {
const decoratedValue = decorateRequirementWithCourses(migration.newValue);
const fullyDecoratedMigration = {
...migration,
newValue: decorateRequirementWithExams(decoratedValue),
};
return fullyDecoratedMigration;
}
return migration;
};

const decorateMigrations = (
migrations: readonly RequirementMigration[]
): readonly MigrationWithDecoratedRequirement[] => migrations.map(decorateMigrationValue);

const generateDecoratedRequirementsJson = (): DecoratedRequirementsJson => {
const { university, college, major, minor, grad } = sourceRequirements;
type MutableDecoratedJson = {
Expand Down Expand Up @@ -355,15 +375,25 @@ const generateDecoratedRequirementsJson = (): DecoratedRequirementsJson => {
};
});
Object.entries(major).forEach(([majorName, majorRequirement]) => {
const { requirements, advisors, specializations, abbrev: abbr, ...rest } = majorRequirement;
const {
requirements,
migrations,
advisors,
specializations,
abbrev: abbr,
...rest
} = majorRequirement;
decoratedJson.major[majorName] = {
...rest,
requirements: decorateRequirements(requirements),
migrations: migrations
? (decorateMigrations(migrations) as RequirementMigration[])
: undefined,
specializations: specializations && decorateRequirements(specializations),
};
});
Object.entries(minor).forEach(([minorName, minorRequirement]) => {
const { requirements, advisors, abbrev: abbr, ...rest } = minorRequirement;
const { requirements, migrations, advisors, abbrev: abbr, ...rest } = minorRequirement;
decoratedJson.minor[minorName] = {
...rest,
requirements: decorateRequirements(requirements),
Expand Down
10 changes: 10 additions & 0 deletions src/requirements/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,23 @@ export type CollegeRequirements<R> = {
};
};

export type typeOfMigration = 'Modify' | 'Delete' | 'Add';

export type RequirementMigration = {
entryYear: number /** This migration applies to students with an entryYear equal to or EARLIER this entry year */;
type: typeOfMigration /** Modify or Delete Migration? This field must already exist in requirements file */;
fieldName: string;
newValue?: CollegeOrMajorRequirement /** Required for modify and add migrations */;
};

export type Major<R> = Readonly<{
name: string;
schools: readonly string[];
requirements: readonly R[];
/** College requirements that have been "specialized" for this major */
specializations?: readonly R[];
advisors?: AdvisorGroup;
migrations?: RequirementMigration[];
readonly abbrev?: string;
}>;

Expand Down