Skip to content

Commit

Permalink
Requirement builder with the new data format (#588)
Browse files Browse the repository at this point in the history
  • Loading branch information
SamChou19815 authored Dec 4, 2021
1 parent 15502f2 commit b448fc0
Show file tree
Hide file tree
Showing 18 changed files with 365 additions and 578 deletions.
21 changes: 14 additions & 7 deletions src/components/Modals/NewCourse/NewCourseModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export default defineComponent({
components: { CourseSelector, TeleportModal, SelectedRequirementEditor },
emits: {
'close-course-modal': () => true,
'add-course': (course: CornellCourseRosterCourse, requirementID: string) =>
typeof course === 'object' && typeof requirementID === 'string',
'add-course': (course: CornellCourseRosterCourse, choice: FirestoreCourseOptInOptOutChoices) =>
typeof course === 'object' && typeof choice === 'object',
},
data() {
return {
Expand All @@ -78,9 +78,6 @@ export default defineComponent({
rightButtonText(): string {
return this.editMode ? 'Next' : 'Add';
},
selectableRequirementChoices(): AppSelectableRequirementChoices {
return store.state.selectableRequirementChoices;
},
},
methods: {
selectCourse(result: CornellCourseRosterCourse) {
Expand All @@ -98,7 +95,7 @@ export default defineComponent({
selectedCourse,
store.state.groupedRequirementFulfillmentReport,
store.state.toggleableRequirementChoices,
/* deprecated AppOverriddenFulfillmentChoices */ {}
store.state.overriddenFulfillmentChoices
);
const requirementsThatAllowDoubleCounting: string[] = [];
Expand Down Expand Up @@ -137,7 +134,17 @@ export default defineComponent({
},
addCourse() {
if (this.selectedCourse == null) return;
this.$emit('add-course', this.selectedCourse, this.selectedRequirementID);
this.$emit('add-course', this.selectedCourse, {
// Only exclude the selected requirement from opt-out.
optOut: this.relatedRequirements
.filter(it => it.id !== this.selectedRequirementID)
.map(it => it.id),
// Only include the selected requirement from opt-in.
acknowledgedCheckerWarningOptIn: this.selfCheckRequirements
.filter(it => it.id === this.selectedRequirementID)
.map(it => it.id),
arbitraryOptIn: {},
});
this.closeCurrentModal();
},
onSelectedChange(selected: string) {
Expand Down
125 changes: 73 additions & 52 deletions src/components/Requirements/IncompleteSelfCheck.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ import store from '@/store';
import {
cornellCourseRosterCourseToFirebaseSemesterCourseWithGlobalData,
addCourseToSemester,
addCourseToSelectableRequirements,
updateRequirementChoice,
} from '@/global-firestore-data';
import { canFulfillChecker } from '@/requirements/requirement-frontend-utils';
import {
canFulfillChecker,
getAllEligibleRelatedRequirementIds,
} from '@/requirements/requirement-frontend-utils';
import NewSelfCheckCourseModal from '@/components/Modals/NewCourse/NewSelfCheckCourseModal.vue';
Expand Down Expand Up @@ -74,61 +77,46 @@ export default defineComponent({
// and courses that do not fulfill the requirement checker
selfCheckCourses(): Record<string, FirestoreSemesterCourse> {
const courses: Record<string, FirestoreSemesterCourse> = {};
store.state.semesters.forEach(semester => {
semester.courses.forEach(course => {
const selectableRequirementCourses =
store.state.derivedSelectableRequirementData.requirementToCoursesMap[this.subReqId];
store.state.semesters
.flatMap(it => it.courses)
.forEach(course => {
if (
!canFulfillChecker(
store.state.userRequirementsMap,
store.state.toggleableRequirementChoices,
this.subReqId,
course.crseId
)
) {
// If the course can't help fulfill the checker, do not add to choices.
return;
}
// if course is mapped to another req(s), only allow it if all other reqs are double countable
let isAddable = true;
const otherReqsMappedTo = store.state.requirementFulfillmentGraph.getConnectedRequirementsFromCourse(
const currentlyMatchedRequirements = store.state.requirementFulfillmentGraph.getConnectedRequirementsFromCourse(
{ uniqueId: course.uniqueID }
);
// true if all other requirements (if any) the course is assigned to are double countable, false otherwise
let allOtherReqsDoubleCountableIfAny = true;
// true if this requirement is double countable, false otherwise.
let thisReqDoubleCountable = false;
// loop through all reqs and determine if all other reqs this course is assigned to are
// double countable (if any exist) and whether or not this req itself is double countable
const collegesMajorsMinors = store.state.groupedRequirementFulfillmentReport;
collegesMajorsMinors.forEach(reqGroup => {
reqGroup.reqs.forEach(req => {
if (
otherReqsMappedTo.includes(req.requirement.id) &&
!req.requirement.allowCourseDoubleCounting
) {
allOtherReqsDoubleCountableIfAny = false;
} else if (
req.requirement.id === this.subReqId &&
req.requirement.allowCourseDoubleCounting
) {
thisReqDoubleCountable = true;
}
});
});
// if neither the current req or all other assigned reqs are not double countable, restrict from adding
if (!(allOtherReqsDoubleCountableIfAny || thisReqDoubleCountable)) {
isAddable = false;
if (currentlyMatchedRequirements.includes(this.subReqId)) {
// If the course is already matched to the current requirement, do not add to choices.
return;
}
const isAlreadyAddedToReq =
selectableRequirementCourses && selectableRequirementCourses.includes(course);
// filter out courses that cannot fulfill the self-check, for self-checks with warnings
const canFulfillReq = canFulfillChecker(
store.state.userRequirementsMap,
store.state.toggleableRequirementChoices,
this.subReqId,
course.crseId
);
const currentRequirementAllowDoubleCounting =
store.state.userRequirementsMap[this.subReqCourseId]?.allowCourseDoubleCounting;
const allOtherRequirementsAllowDoubleCounting = store.state.requirementFulfillmentGraph
.getConnectedRequirementsFromCourse({ uniqueId: course.uniqueID })
.every(reqID => store.state.userRequirementsMap[reqID]?.allowCourseDoubleCounting);
if (!currentRequirementAllowDoubleCounting && !allOtherRequirementsAllowDoubleCounting) {
// At this point, we need to consider double counting issues.
// There are 2 ways we can add the course to the requirement without double counting violations:
// 1. This requirement allows double counting.
// 2. All the already matched requirements allow double counting.
// If both don't hold, we cannot add.
return;
}
if (!isAlreadyAddedToReq && isAddable && canFulfillReq) courses[course.code] = course;
// All pre-conditions have been checked, we can add it as choice!
courses[course.code] = course;
});
});
return courses;
},
Expand All @@ -150,12 +138,45 @@ export default defineComponent({
},
addExistingCourse(option: string) {
this.showDropdown = false;
addCourseToSelectableRequirements(this.selfCheckCourses[option].uniqueID, this.subReqId);
updateRequirementChoice(this.selfCheckCourses[option].uniqueID, choice => ({
...choice,
// Since we edit from a self-check requirement,
// we know it must be `acknowledgedCheckerWarningOptIn`.
acknowledgedCheckerWarningOptIn: Array.from(
new Set([...choice.acknowledgedCheckerWarningOptIn, this.subReqId])
),
// Keep existing behavior of keeping it connected to at most one requirement.
optOut: getAllEligibleRelatedRequirementIds(
this.selfCheckCourses[option].crseId,
store.state.groupedRequirementFulfillmentReport,
store.state.toggleableRequirementChoices
),
}));
},
addNewCourse(course: CornellCourseRosterCourse, season: FirestoreSemesterSeason, year: number) {
this.showDropdown = false;
const newCourse = cornellCourseRosterCourseToFirebaseSemesterCourseWithGlobalData(course);
addCourseToSemester(year, season, newCourse, this.subReqId, this.$gtag);
addCourseToSemester(
year,
season,
newCourse,
// Since the course is new, we know the old choice does not exist.
() => ({
arbitraryOptIn: {},
// Since we edit from a self-check requirement,
// we know it must be `acknowledgedCheckerWarningOptIn`.
acknowledgedCheckerWarningOptIn: [this.subReqId],
// We also need to opt-out of all requirements without warnings,
// because the user intention is clear that we only want to bind
// the course to this specific requirement.
optOut: getAllEligibleRelatedRequirementIds(
newCourse.crseId,
store.state.groupedRequirementFulfillmentReport,
store.state.toggleableRequirementChoices
),
}),
this.$gtag
);
},
openCourseModal() {
this.isCourseModalOpen = true;
Expand Down
11 changes: 3 additions & 8 deletions src/components/Requirements/RequirementSelfCheckSlots.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import IncompleteSelfCheck from '@/components/Requirements/IncompleteSelfCheck.v
import store from '@/store';
import {
convertFirestoreSemesterCourseToCourseTaken,
getMatchedRequirementFulfillmentSpecification,
courseIsAPIB,
} from '@/requirements/requirement-frontend-utils';
Expand All @@ -39,13 +38,9 @@ export default defineComponent({
computed: {
fulfilledSelfCheckCourses(): readonly CourseTaken[] {
// selectedCourses are courses that fulfill the requirement based on user-choice
// they are taken from derivedSelectableRequirementData
const selectedFirestoreCourses =
store.state.derivedSelectableRequirementData.requirementToCoursesMap[
this.requirementFulfillment.requirement.id
] || [];
const selectedCourses = selectedFirestoreCourses.map(
convertFirestoreSemesterCourseToCourseTaken
// they are taken from requirement graph
const selectedCourses = store.state.requirementFulfillmentGraph.getConnectedCoursesFromRequirement(
this.requirementFulfillment.requirement.id
);
// fulfillableCourses are the courses that can fulfill this requirement
Expand Down
49 changes: 41 additions & 8 deletions src/components/Semester/Semester.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,10 @@ import {
addCourseToSemester,
deleteCourseFromSemester,
deleteAllCoursesFromSemester,
addCoursesToSelectableRequirements,
updateRequirementChoices,
} from '@/global-firestore-data';
import { updateSubjectColorData } from '@/store';
import store, { updateSubjectColorData } from '@/store';
import { getAllEligibleRelatedRequirementIds } from '@/requirements/requirement-frontend-utils';
type ComponentRef = { $el: HTMLDivElement };
Expand Down Expand Up @@ -229,6 +230,15 @@ export default defineComponent({
get(): readonly FirestoreSemesterCourse[] {
return this.courses;
},
/**
* This function is called when a course is dragged into the semester.
*
* It can be a semester-to-semester drag-n-drop, which does not have `requirementID`
* and does not require update the requirement.
* It can also be a requirement-bar-to-semester drag-n-drop, which has a `requirementID`
* attached to the course. We need to check the presence of this field and update requirement
* choice accordingly.
*/
set(newCourses: readonly AppFirestoreSemesterCourseWithRequirementID[]) {
const courses = newCourses.map(({ requirementID: _, ...rest }) => rest);
editSemester(
Expand All @@ -239,11 +249,33 @@ export default defineComponent({
courses,
})
);
const newChoices: Record<string, string> = {};
newCourses.forEach(({ uniqueID, requirementID }) => {
if (requirementID) newChoices[uniqueID] = requirementID;
updateRequirementChoices(oldChoices => {
const choices = { ...oldChoices };
newCourses.forEach(({ uniqueID, requirementID, crseId }) => {
if (requirementID == null) {
// In this case, it's not a course from requirement bar
return;
}
const choice = choices[uniqueID] || {
arbitraryOptIn: {},
acknowledgedCheckerWarningOptIn: [],
optOut: [],
};
// We know the requirement must be dragged from requirements without warnings,
// because only those courses provide suggested courses.
// As a result, `acknowledgedCheckerWarningOptIn` is irrelevant and we only need to update
// the `optOut` field.
// Below, we find all the requirements it can possibly match,
// and only remove the requirementID since that's the one we should keep.
const optOut = getAllEligibleRelatedRequirementIds(
crseId,
store.state.groupedRequirementFulfillmentReport,
store.state.toggleableRequirementChoices
).filter(it => it !== requirementID);
choices[uniqueID] = { ...choice, optOut };
});
return choices;
});
addCoursesToSelectableRequirements(newChoices);
},
},
// Add space for a course if there is a "shadow" of it, decrease if it is from the current sem
Expand Down Expand Up @@ -330,9 +362,10 @@ export default defineComponent({
closeConfirmationModal() {
this.isConfirmationOpen = false;
},
addCourse(data: CornellCourseRosterCourse, requirementID: string) {
addCourse(data: CornellCourseRosterCourse, choice: FirestoreCourseOptInOptOutChoices) {
const newCourse = cornellCourseRosterCourseToFirebaseSemesterCourseWithGlobalData(data);
addCourseToSemester(this.year, this.season, newCourse, requirementID, this.$gtag);
// Since the course is new, we know the old choice does not exist.
addCourseToSemester(this.year, this.season, newCourse, () => choice, this.$gtag);
const courseCode = `${data.subject} ${data.catalogNbr}`;
this.openConfirmationModal(`Added ${courseCode} to ${this.season} ${this.year}`);
Expand Down
4 changes: 0 additions & 4 deletions src/firebase-admin-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ export const toggleableRequirementChoicesCollection = db
.collection('user-toggleable-requirement-choices')
.withConverter(getTypedFirestoreDataConverter<AppToggleableRequirementChoices>());

export const selectableRequirementChoicesCollection = db
.collection('user-selectable-requirement-choices')
.withConverter(getTypedFirestoreDataConverter<AppSelectableRequirementChoices>());

export const overriddenFulfillmentChoicesCollection = db
.collection('user-overridden-fulfillment-choices')
.withConverter(getTypedFirestoreDataConverter<FirestoreOverriddenFulfillmentChoices>());
Expand Down
4 changes: 0 additions & 4 deletions src/firebase-frontend-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,6 @@ export const toggleableRequirementChoicesCollection = db
.collection('user-toggleable-requirement-choices')
.withConverter(getTypedFirestoreDataConverter<AppToggleableRequirementChoices>());

export const selectableRequirementChoicesCollection = db
.collection('user-selectable-requirement-choices')
.withConverter(getTypedFirestoreDataConverter<AppSelectableRequirementChoices>());

export const overriddenFulfillmentChoicesCollection = db
.collection('user-overridden-fulfillment-choices')
.withConverter(getTypedFirestoreDataConverter<FirestoreOverriddenFulfillmentChoices>());
Expand Down
8 changes: 4 additions & 4 deletions src/global-firestore-data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export {
} from './semesters';
export { default as chooseToggleableRequirementOption } from './toggleable-requirement-choices';
export {
addCourseToSelectableRequirements,
addCoursesToSelectableRequirements,
deleteCourseFromSelectableRequirements,
} from './selectable-requirement-choices';
updateRequirementChoice,
updateRequirementChoices,
deleteCourseFromRequirementChoices,
} from './override-fulfillment-choices';
46 changes: 46 additions & 0 deletions src/global-firestore-data/override-fulfillment-choices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { overriddenFulfillmentChoicesCollection } from '../firebase-frontend-config';
import store from '../store';

export const updateRequirementChoice = (
courseUniqueID: string | number,
choiceUpdater: (choice: FirestoreCourseOptInOptOutChoices) => FirestoreCourseOptInOptOutChoices
): void => {
overriddenFulfillmentChoicesCollection.doc(store.state.currentFirebaseUser.email).set({
...store.state.overriddenFulfillmentChoices,
[courseUniqueID]: choiceUpdater(
store.state.overriddenFulfillmentChoices[courseUniqueID] || {
arbitraryOptIn: {},
acknowledgedCheckerWarningOptIn: [],
optOut: [],
}
),
});
};

export const updateRequirementChoices = (
updater: (
oldChoices: FirestoreOverriddenFulfillmentChoices
) => FirestoreOverriddenFulfillmentChoices
): void => {
overriddenFulfillmentChoicesCollection
.doc(store.state.currentFirebaseUser.email)
.set(updater(store.state.overriddenFulfillmentChoices));
};

export const deleteCourseFromRequirementChoices = (courseUniqueID: string | number): void =>
deleteCoursesFromRequirementChoices([courseUniqueID]);

export const deleteCoursesFromRequirementChoices = (
courseUniqueIds: readonly (string | number)[]
): void => {
const courseUniqueIdStrings = new Set(courseUniqueIds.map(uniqueId => uniqueId.toString()));
overriddenFulfillmentChoicesCollection
.doc(store.state.currentFirebaseUser.email)
.set(
Object.fromEntries(
Object.entries(store.state.overriddenFulfillmentChoices).filter(
([uniqueId]) => !courseUniqueIdStrings.has(uniqueId)
)
)
);
};
Loading

0 comments on commit b448fc0

Please sign in to comment.