diff --git a/src/components/planner/Sidebar/Sidebar.tsx b/src/components/planner/Sidebar/Sidebar.tsx index 449080316..2b798e02b 100644 --- a/src/components/planner/Sidebar/Sidebar.tsx +++ b/src/components/planner/Sidebar/Sidebar.tsx @@ -18,6 +18,7 @@ import { Course, DraggableCourse, GetDragIdByCourse } from '../types'; import useFuse from '../useFuse'; import 'react-loading-skeleton/dist/skeleton.css'; +import { useSemestersContext } from '@components/planner/SemesterContext'; export interface CourseSelectorContainerProps { planId: string; @@ -36,18 +37,26 @@ function CourseSelectorContainer({ getRequirementDragId, courseDragged, }: CourseSelectorContainerProps) { - const { data: validationData, error } = trpc.validator.degreeValidator.useQuery(planId, { - // TODO: Fix validator retries. - // Validator kept retrying when the "REPORT ERROR" button was clicked on. At some point it should stop retrying. - retry: false, - trpc: { - context: { - // Isolate the validation request from others. Validator is an external service so its reliability - // should not affect other requets. - skipBatch: true, + const { allSemesters: semesters } = useSemestersContext(); + const { data: validationData, error } = trpc.validator.degreeValidator.useQuery( + { + planId, + // If they started during a Spring semester, it should use the previous numeric year's degree plan + startYear: semesters[0].code.year - (semesters[0].code.semester === 's' ? 1 : 0), + }, + { + // TODO: Fix validator retries. + // Validator kept retrying when the "REPORT ERROR" button was clicked on. At some point it should stop retrying. + retry: false, + trpc: { + context: { + // Isolate the validation request from others. Validator is an external service so its reliability + // should not affect other requets. + skipBatch: true, + }, }, }, - }); + ); const validatorError = useMemo( () => error?.data?.code === 'INTERNAL_SERVER_ERROR', diff --git a/src/server/trpc/router/validator.ts b/src/server/trpc/router/validator.ts index 77b49326b..6522d6a5b 100644 --- a/src/server/trpc/router/validator.ts +++ b/src/server/trpc/router/validator.ts @@ -310,115 +310,117 @@ export const validatorRouter = router({ } }), // Protected route: ensures session user is same as plan owner - degreeValidator: protectedProcedure.input(z.string().min(1)).query(async ({ ctx, input }) => { - // Fetch current plan - const planData = await ctx.prisma.plan.findUnique({ - where: { - id: input, - }, - select: { - name: true, - id: true, - userId: true, - semesters: { - include: { - courses: true, + degreeValidator: protectedProcedure + .input(z.object({ planId: z.string().min(1), startYear: z.number() })) + .query(async ({ ctx, input: { planId, startYear } }) => { + // Fetch current plan + const planData = await ctx.prisma.plan.findUnique({ + where: { + id: planId, + }, + select: { + name: true, + id: true, + userId: true, + semesters: { + include: { + courses: true, + }, }, + transferCredits: true, }, - transferCredits: true, - }, - }); - - if (!planData) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Plan not found', }); - } - if (ctx.session.user.id !== planData.userId) { - throw new TRPCError({ code: 'FORBIDDEN' }); - } + if (!planData) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Plan not found', + }); + } - const { semesters, transferCredits } = planData; - - // Get degree requirements - const degreeRequirements = await ctx.prisma.degreeRequirements.findFirst({ - where: { - plan: { id: planData.id }, - }, - }); - - // Get bypasses - const bypasses = degreeRequirements?.bypasses ?? []; - - // Remove invalidCourses - const removeInvalidCoursesFromSemesters = () => { - return semesters.map((sem) => { - const courses = sem.courses - .reduce((acc, curr) => [...acc, curr.code], [] as string[]) - .filter((c) => { - const [possiblePrefix, possibleCode] = c.split(' '); - if (Number.isNaN(Number(possibleCode)) || !Number.isNaN(Number(possiblePrefix))) { - return false; - } - return true; - }); - return { ...sem, courses }; - }); - }; + if (ctx.session.user.id !== planData.userId) { + throw new TRPCError({ code: 'FORBIDDEN' }); + } - const semestersWithCourses = removeInvalidCoursesFromSemesters(); + const { semesters, transferCredits } = planData; - if (!degreeRequirements?.major || degreeRequirements.major === 'undecided') { - return { plan: planData, validation: [], bypasses: [] }; - } + // Get degree requirements + const degreeRequirements = await ctx.prisma.degreeRequirements.findFirst({ + where: { + plan: { id: planData.id }, + }, + }); - // TODO: will we always ignore odd credits such as 'PSY 1---'? - const regex = /([a-z0-9])* ([a-z0-9]){4}$/gi; - const validTransferCredits = transferCredits.filter((credit) => credit.match(regex) !== null); - - const body = { - courses: [...semestersWithCourses.flatMap((s) => s.courses), ...validTransferCredits], - requirements: { - year: 2022, - majors: [degreeRequirements.major], - minors: [], - }, - bypasses, - }; - - const res = await fetch(`${env.NEXT_PUBLIC_VALIDATOR}/validate`, { - method: 'POST', - body: JSON.stringify(body), - headers: { - 'content-type': 'application/json', - }, - }); - - if (!res.ok) { - const errorMsg = await res.json(); - if (res.status == 404) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: errorMsg.error, - cause: new DegreeNotFound(errorMsg), + // Get bypasses + const bypasses = degreeRequirements?.bypasses ?? []; + + // Remove invalidCourses + const removeInvalidCoursesFromSemesters = () => { + return semesters.map((sem) => { + const courses = sem.courses + .reduce((acc, curr) => [...acc, curr.code], [] as string[]) + .filter((c) => { + const [possiblePrefix, possibleCode] = c.split(' '); + if (Number.isNaN(Number(possibleCode)) || !Number.isNaN(Number(possiblePrefix))) { + return false; + } + return true; + }); + return { ...sem, courses }; }); + }; + + const semestersWithCourses = removeInvalidCoursesFromSemesters(); + + if (!degreeRequirements?.major || degreeRequirements.major === 'undecided') { + return { plan: planData, validation: [], bypasses: [] }; } - const error = new DegreeValidationError( - `Validator fetch failed with stats: ${res.status}, err: ${errorMsg}`, - ); - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: error.message, - cause: error, + // TODO: will we always ignore odd credits such as 'PSY 1---'? + const regex = /([a-z0-9])* ([a-z0-9]){4}$/gi; + const validTransferCredits = transferCredits.filter((credit) => credit.match(regex) !== null); + + const body = { + courses: [...semestersWithCourses.flatMap((s) => s.courses), ...validTransferCredits], + requirements: { + year: startYear, + majors: [degreeRequirements.major], + minors: [], + }, + bypasses, + }; + + const res = await fetch(`${env.NEXT_PUBLIC_VALIDATOR}/validate`, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'content-type': 'application/json', + }, }); - } - const validationData = await res.json(); - return { plan: planData, validation: validationData, bypasses }; - }), + if (!res.ok) { + const errorMsg = await res.json(); + if (res.status == 404) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: errorMsg.error, + cause: new DegreeNotFound(errorMsg), + }); + } + + const error = new DegreeValidationError( + `Validator fetch failed with stats: ${res.status}, err: ${errorMsg}`, + ); + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: error.message, + cause: error, + }); + } + + const validationData = await res.json(); + return { plan: planData, validation: validationData, bypasses }; + }), }); type CourseOptions = { diff --git a/validator/degree_solver.py b/validator/degree_solver.py index 4d79f5d7c..10e0533b8 100644 --- a/validator/degree_solver.py +++ b/validator/degree_solver.py @@ -187,7 +187,8 @@ def load_requirements( # Logic for adding majors for input_req in degree_requirements_input.majors: # Get major data from json - year = degree_requirements_input.year + # We don't support degrees earlier than 2022 + year = max([degree_requirements_input.year, 2022]) if input_req not in degree_plans[str(year)]: # Check if the years before this one have it y = year @@ -209,6 +210,7 @@ def load_requirements( year != degree_requirements_input.year ): # The using_year has been replaced to a working year break + print("Error: Could not find the degree") raise DegreeNotFoundException requirements_data = degree_plans[str(year)][input_req]["requirements"][ "major"