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

Load Requirement Ranking for Frontend #876

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/Requirements/RequirementFulfillment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
:isCompleted="isCompleted"
:displayDescription="displayDescription"
:toggleableRequirementChoice="toggleableRequirementChoice"
:requirement="requirementFulfillment.requirement"
@onShowAllCourses="onShowAllCourses"
/>
<requirement-self-check-slots
Expand Down
71 changes: 70 additions & 1 deletion src/components/Requirements/RequirementFulfillmentSlots.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ type IncompleteSubReqCourseSlot = {

type SubReqCourseSlot = CompletedSubReqCourseSlot | IncompleteSubReqCourseSlot;

/**
* Generate a list of incomplete courses for a requirement. This is used to display the courses
* that the user has not taken yet for a requirement. The courses are generated from the list of
* eligible courses for the requirement.
* @param allTakenCourseIds A set of all course IDs that the user has taken.
* @param eligibleCourseIds A list of course IDs that are eligible for the requirement.
* @param requirementID The ID of the requirement.
* @returns A list of incomplete courses for the requirement.
*/
const generateSubReqIncompleteCourses = (
allTakenCourseIds: ReadonlySet<number>,
eligibleCourseIds: readonly number[],
Expand Down Expand Up @@ -77,6 +86,7 @@ export default defineComponent({
isCompleted: { type: Boolean, required: true },
displayDescription: { type: Boolean, required: true },
toggleableRequirementChoice: { type: String, default: null },
requirement: { type: Object as PropType<RequirementWithIDSourceType>, required: true },
},
emits: {
onShowAllCourses(courses: {
Expand Down Expand Up @@ -174,10 +184,69 @@ export default defineComponent({
});
}

return slots;
// The following code block is to sort the courses in each slot by the ranking of the courses.
// The ranking is stored in the Vuex store. The ranking is an array of course IDs, sorted by
// the number of students who have taken the course. The course with the most students who
// have taken the course is ranked first, and the course with the least students who have
// taken the course is ranked last.
// The ranking is computed by the script '/script/gen-req-full-stats.ts'.

// Sort the incomplete slots by ranking
const sortedIncompleteReq: SubReqCourseSlot[] = [];
// Get the ranking from the Vuex store for the requirement ID
const ranking = store.state.requirementRanking.get(
this.requirement.id.replace('[FORWARD_SLASH]', '/')
);
// If the ranking exists, sort the courses in each slot by the ranking
if (ranking) {
// Sort the slots incomplete slots by ranking, and push them to the sortedIncompleteReq
// NOTE: The completed slots are not sorted by ranking because they are already completed
// We must use a for loop instead of a foreach loop because order matters
// slots in rankings are in specific order
for (let k = 0; k < slots.length; k += 1) {
const slot = slots[k];

// If the slot is completed, push it to the sortedIncompleteReq
if (slot.isCompleted) {
sortedIncompleteReq.push(slot);
} else {
// Sort the courses in the slot by ranking
const courses = [...slot.courses];
const sortedCourses = courses.sort((a, b) => {
let aRank = 0;
let bRank = 0;
// Find the ranking of the course
for (let i = 0; i < ranking.length; i += 1) {
if (a.crseId === ranking[i]) {
aRank = i;
}
if (b.crseId === ranking[i]) {
bRank = i;
}
}
// sorted by who has the higher rank
return aRank - bRank;
});
// Push the sorted slot to the sortedIncompleteReq
sortedIncompleteReq.push({
name: slot.name,
isCompleted: false,
courses: sortedCourses,
});
}
}
} else {
// ranking data not yet computed. Need to run '/script/gen-req-full-stats.ts'
}

return sortedIncompleteReq; // return the sorted slots
},
},
methods: {
/**
* Emit an event to show all courses for a requirement.
* @param subReqIndex The index of the sub-requirement.
*/
onShowAllCourses(subReqIndex: number) {
this.$emit('onShowAllCourses', {
requirementName: this.requirementFulfillment.requirement.name,
Expand Down
2 changes: 2 additions & 0 deletions src/firebase-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,5 @@ export const onboardingDataCollection = collection(db, 'user-onboarding-data').w
export const trackUsersCollection = collection(db, 'track-users').withConverter(
getTypedFirestoreDataConverter<FirestoreTrackUsersData>()
);

export const courseFulfillmentCollection = collection(db, 'course-fulfillment-stats');
38 changes: 37 additions & 1 deletion src/store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Store } from 'vuex';
import { doc, getDoc, onSnapshot, setDoc, updateDoc } from 'firebase/firestore';
import { doc, getDoc, getDocs, onSnapshot, setDoc, updateDoc } from 'firebase/firestore';

import * as fb from './firebase-config';
import computeGroupedRequirementFulfillmentReports from './requirements/requirement-frontend-computation';
Expand Down Expand Up @@ -50,6 +50,7 @@ export type VuexStoreState = {
subjectColors: Readonly<Record<string, string>>;
uniqueIncrementer: number;
isTeleportModalOpen: boolean;
requirementRanking: ReadonlyMap<string, number[]>;
};

export class TypedVuexStore extends Store<VuexStoreState> {}
Expand Down Expand Up @@ -96,9 +97,21 @@ const store: TypedVuexStore = new TypedVuexStore({
subjectColors: {},
uniqueIncrementer: 0,
isTeleportModalOpen: false,
requirementRanking: new Map(),
},
actions: {},
mutations: {
/**
* Sets the requirementRanking in the store.
* @param state The Vuex store state.
* @param requirementRanking The requirementRanking to set.
*/
setRequirementRanking(
state: VuexStoreState,
requirementRanking: ReadonlyMap<string, number[]>
) {
state.requirementRanking = requirementRanking;
},
setCurrentFirebaseUser(state: VuexStoreState, user: SimplifiedFirebaseUser) {
state.currentFirebaseUser = user;
},
Expand Down Expand Up @@ -224,6 +237,11 @@ const autoRecomputeDerivedData = (): (() => void) =>
}
});

/**
* Initializes the Firestore listeners for the current user.
* @param onLoad Callback to be called when all Firestore listeners are loaded.
* @returns A function to unsubscribe all Firestore listeners.
*/
export const initializeFirestoreListeners = (onLoad: () => void): (() => void) => {
const simplifiedUser = store.state.currentFirebaseUser;

Expand Down Expand Up @@ -354,6 +372,24 @@ export const initializeFirestoreListeners = (onLoad: () => void): (() => void) =
uniqueIncrementerUnsubscriber();
derivedDataComputationUnsubscriber();
};

// Populate the Vuex store with requirementRankgs
getDocs(fb.courseFulfillmentCollection).then(snapshot => {
const requirementRanking: Map<string, number[]> = new Map();
for (let i = 0; i < snapshot.docs.length; i += 1) {
const { id, data } = snapshot.docs[i];

// For each requirement, create a ranking of courses
const ranking: number[] = [];
for (let j = 0; j < Object.entries(data).length; j += 1) {
const crseId = Object.entries(data)[j][1]; // crseId is the course ID
ranking.push(crseId); // push the course ID into the ranking
}

requirementRanking.set(id, ranking); // set the ranking for the requirement
}
store.commit('setRequirementRanking', requirementRanking); // set the requirementRanking in the store
});
return unsubscriber;
};

Expand Down
Loading