Skip to content

Commit 3503759

Browse files
committed
feat(EWS): [wip] actor summaries
1 parent 01b07f9 commit 3503759

File tree

3 files changed

+285
-7
lines changed

3 files changed

+285
-7
lines changed

lib/AnalyticsDataProcessor.ts

Lines changed: 238 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ import calcADAPTScores from "./models/calcADAPTScores";
1212
import ewsCourseSummary, {
1313
IEWSCourseSummary_Raw,
1414
} from "./models/ewsCourseSummary";
15+
import ewsActorSummary, {
16+
IEWSActorSummary_Raw,
17+
} from "./models/ewsActorSummary";
1518
import calcADAPTInteractionDays from "./models/calcADAPTInteractionDays";
1619
import calcADAPTAssignments from "./models/calcADAPTAssignments";
20+
import enrollments from "./models/enrollments";
1721

1822
class AnalyticsDataProcessor {
1923
constructor() {}
@@ -30,6 +34,7 @@ class AnalyticsDataProcessor {
3034
//await this.compressTexbookInteractionsByDate();
3135
//await this.compressTextbookNumInteractions(); // Should be ran after compressing textbookInteractionsByDate
3236
await this.writeEWSCourseSummary();
37+
await this.writeEWSActorSummary();
3338
}
3439

3540
private async compressADAPTAllAssignments(): Promise<boolean> {
@@ -101,24 +106,29 @@ class AnalyticsDataProcessor {
101106
await connectDB();
102107

103108
debugADP("[compressADAPTAssignments]: Starting aggregation...");
104-
await ADAPT.aggregate(
109+
await Gradebook.aggregate(
105110
[
106111
{
107112
$match: {
108113
assignment_id: {
109114
$exists: true,
110-
$ne: "",
115+
$nin: [null, ""],
111116
},
112117
},
113118
},
114119
{
115120
$group: {
116121
_id: {
117-
courseID: "$course_id",
118-
actor: "$anon_student_id",
122+
courseID: "$class",
123+
actor: "$email",
119124
},
120125
assignments: {
121-
$addToSet: "$assignment_id",
126+
$addToSet: {
127+
assignment_id: {
128+
$toString: "$assignment_id",
129+
},
130+
score: "$assignment_percent",
131+
},
122132
},
123133
},
124134
},
@@ -133,6 +143,18 @@ class AnalyticsDataProcessor {
133143
},
134144
},
135145
},
146+
{
147+
$match: {
148+
actor: {
149+
$exists: true,
150+
$nin: [null, ""],
151+
},
152+
courseID: {
153+
$exists: true,
154+
$nin: [null, ""],
155+
},
156+
},
157+
},
136158
{
137159
$merge: {
138160
into: "calcADAPTAssignments",
@@ -678,6 +700,7 @@ class AnalyticsDataProcessor {
678700
try {
679701
await connectDB();
680702

703+
debugADP("[writeEWSCourseSummary]: Starting aggregation...");
681704
const coursesWAssignments = await calcADAPTAllAssignments.find();
682705
const courseAssignmentMap = new Map<string, string[]>();
683706
coursesWAssignments.forEach((course) => {
@@ -844,6 +867,7 @@ class AnalyticsDataProcessor {
844867
}))
845868
);
846869

870+
debugADP(`[writeEWSCourseSummary]: Finished writing course summaries.`);
847871
return true;
848872
} catch (err: any) {
849873
debugADP(
@@ -852,6 +876,215 @@ class AnalyticsDataProcessor {
852876
return false;
853877
}
854878
}
879+
880+
private async writeEWSActorSummary(): Promise<boolean> {
881+
try {
882+
await connectDB();
883+
884+
debugADP("[writeEWSActorSummary]: Starting aggregation...");
885+
const actors = await enrollments.aggregate([
886+
{
887+
$group: {
888+
_id: {
889+
email: "$email",
890+
courseID: "$courseID",
891+
},
892+
},
893+
},
894+
{
895+
$project: {
896+
_id: 0,
897+
actor_id: "$_id.email",
898+
course_id: "$_id.courseID",
899+
},
900+
},
901+
]);
902+
903+
const actorWCourses = new Map<string, string[]>();
904+
actors.forEach((actor) => {
905+
if (actorWCourses.has(actor.actor_id)) {
906+
actorWCourses.get(actor.actor_id)?.push(actor.course_id);
907+
} else {
908+
actorWCourses.set(actor.actor_id, [actor.course_id]);
909+
}
910+
});
911+
912+
const actorAssignments = await calcADAPTAssignments.aggregate(
913+
[
914+
{
915+
$match: {
916+
$or: Array.from(actorWCourses.entries()).map(
917+
([actorID, courseIDs]) => ({
918+
actor: actorID,
919+
courseID: { $in: courseIDs },
920+
})
921+
),
922+
},
923+
},
924+
],
925+
{
926+
allowDiskUse: true,
927+
}
928+
);
929+
930+
const actorSummaries: IEWSActorSummary_Raw[] = [];
931+
for (const [actorID, courseIDs] of Array.from(actorWCourses.entries())) {
932+
for (const courseID of courseIDs) {
933+
const actorCourseAssignments = actorAssignments.filter(
934+
(assignment: { actor: string; courseID: string }) =>
935+
assignment.actor === actorID && assignment.courseID === courseID
936+
);
937+
938+
const actorSummary: IEWSActorSummary_Raw = {
939+
actor_id: actorID,
940+
course_id: courseID,
941+
assignments:
942+
actorCourseAssignments
943+
.at(0)
944+
?.assignments.map(
945+
(assignment: { assignment_id: string; score: number }) => ({
946+
assignment_id: assignment.assignment_id,
947+
score: isNaN(assignment.score) ? 0 : assignment.score,
948+
})
949+
) || [],
950+
percent_seen: 0,
951+
interaction_days: 0,
952+
course_percent: 0,
953+
};
954+
955+
actorSummaries.push(actorSummary);
956+
}
957+
}
958+
959+
const interactionDays = await calcADAPTInteractionDays.aggregate([
960+
{
961+
$group: {
962+
_id: {
963+
actor: "$actor",
964+
courseID: "$courseID",
965+
},
966+
interaction_days: {
967+
$sum: {
968+
$size: "$days",
969+
},
970+
},
971+
},
972+
},
973+
{
974+
$project: {
975+
_id: 0,
976+
actor_id: "$_id.actor",
977+
course_id: "$_id.courseID",
978+
interaction_days: 1,
979+
},
980+
},
981+
]);
982+
983+
interactionDays.forEach((interaction) => {
984+
const actorSummary = actorSummaries.find(
985+
(summary) =>
986+
summary.actor_id === interaction.actor_id &&
987+
summary.course_id === interaction.course_id
988+
);
989+
if (actorSummary) {
990+
actorSummary.interaction_days = interaction.interaction_days;
991+
}
992+
});
993+
994+
const courseAssignments = await calcADAPTAllAssignments.aggregate([
995+
{
996+
$group: {
997+
_id: "$courseID",
998+
assignments_count: {
999+
$sum: {
1000+
$size: "$assignments",
1001+
},
1002+
},
1003+
},
1004+
},
1005+
{
1006+
$project: {
1007+
_id: 0,
1008+
courseID: "$_id",
1009+
assignments_count: 1,
1010+
},
1011+
},
1012+
]);
1013+
1014+
const courseAssignmentsMap = new Map<string, number>();
1015+
courseAssignments.forEach((course) => {
1016+
courseAssignmentsMap.set(course.courseID, course.assignments_count);
1017+
});
1018+
1019+
actorSummaries.forEach((actorSummary) => {
1020+
const courseID = actorSummary.course_id;
1021+
const assignmentsCount = courseAssignmentsMap.get(courseID) ?? 0;
1022+
actorSummary.percent_seen =
1023+
(actorSummary.assignments.length / assignmentsCount) * 100 || 0;
1024+
});
1025+
1026+
// For course_percent, find the latest gradebook entry for each actor in each course and use the overall_course_percent
1027+
const latestGradebookEntries = await Gradebook.aggregate([
1028+
{
1029+
$group: {
1030+
_id: {
1031+
actor: "$email",
1032+
courseID: "$class",
1033+
},
1034+
newestDocument: {
1035+
$last: "$$ROOT",
1036+
},
1037+
},
1038+
},
1039+
{
1040+
$project: {
1041+
_id: 0,
1042+
actor_id: "$_id.actor",
1043+
course_id: "$_id.courseID",
1044+
course_percent: "$newestDocument.overall_course_percent",
1045+
},
1046+
},
1047+
]);
1048+
1049+
latestGradebookEntries.forEach((entry) => {
1050+
const actorSummary = actorSummaries.find(
1051+
(summary) =>
1052+
summary.actor_id === entry.actor_id &&
1053+
summary.course_id === entry.course_id
1054+
);
1055+
if (actorSummary) {
1056+
actorSummary.course_percent = entry.course_percent;
1057+
}
1058+
});
1059+
1060+
// filter missing actor_id and course_id
1061+
const filteredActorSummaries = actorSummaries.filter(
1062+
(summary) => summary.actor_id && summary.course_id
1063+
);
1064+
1065+
// Write the actor summaries to the database
1066+
await ewsActorSummary.bulkWrite(
1067+
filteredActorSummaries.map((summary) => ({
1068+
updateOne: {
1069+
filter: {
1070+
actor_id: summary.actor_id,
1071+
course_id: summary.course_id,
1072+
},
1073+
update: summary,
1074+
upsert: true,
1075+
},
1076+
}))
1077+
);
1078+
1079+
debugADP(`[writeEWSActorSummary]: Finished writing actor summaries.`);
1080+
return true;
1081+
} catch (err: any) {
1082+
debugADP(
1083+
err.message ?? "Unknown error occured while writing EWS actor summary"
1084+
);
1085+
return false;
1086+
}
1087+
}
8551088
}
8561089

8571090
export default AnalyticsDataProcessor;

lib/models/calcADAPTAssignments.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Document, Schema, model, models } from "mongoose";
33
export interface ICalcADAPTAssignments_Raw {
44
actor: string;
55
courseID: string;
6-
assignments: string[];
6+
assignments: { assignment_id: string; score: number }[];
77
assignments_count: number;
88
}
99

@@ -15,7 +15,12 @@ const CalcADAPTAssignmentsSchema = new Schema<ICalcADAPTAssignments>(
1515
{
1616
actor: String,
1717
courseID: String,
18-
assignments: [String],
18+
assignments: [
19+
{
20+
assignment_id: String,
21+
score: Number,
22+
},
23+
],
1924
assignments_count: Number,
2025
},
2126
{

lib/models/ewsActorSummary.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Document, Schema, model, models } from "mongoose";
2+
3+
export interface IEWSActorSummary_Raw {
4+
actor_id: string;
5+
course_id: string;
6+
assignments: { assignment_id: string; score: number }[];
7+
percent_seen: number;
8+
interaction_days: number;
9+
course_percent: number;
10+
}
11+
12+
export interface IEWSActorSummary extends IEWSActorSummary_Raw, Document {}
13+
14+
const EWSActorSummarySchema = new Schema<IEWSActorSummary>(
15+
{
16+
actor_id: { type: String, required: true },
17+
course_id: { type: String, required: true },
18+
assignments: [
19+
{
20+
assignment_id: { type: String, required: true },
21+
score: { type: Number, required: true },
22+
},
23+
],
24+
percent_seen: { type: Number, required: true },
25+
interaction_days: { type: Number, required: true },
26+
course_percent: { type: Number, required: true },
27+
},
28+
{
29+
collection: "ewsActorSummary",
30+
}
31+
);
32+
33+
EWSActorSummarySchema.index({ actor_id: 1, course_id: 1 }, { unique: true });
34+
35+
export default models.EWSActorSummary ||
36+
model<IEWSActorSummary>(
37+
"EWSActorSummary",
38+
EWSActorSummarySchema,
39+
"ewsActorSummary"
40+
);

0 commit comments

Comments
 (0)