Skip to content

Commit 91f82fa

Browse files
Merge pull request #1866 from OneCommunityGlobal/feature/education-progress-api
Mani shashank feat: Add Education Progress API endpoints
2 parents d2e4acc + 88e1c9c commit 91f82fa

4 files changed

Lines changed: 327 additions & 6 deletions

File tree

src/controllers/progressController.js

Lines changed: 268 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,114 @@
1-
// const mongoose = require('mongoose');
1+
const mongoose = require('mongoose');
22
const Progress = require('../models/progress');
33
const UserProfile = require('../models/userProfile');
44
const Atom = require('../models/atom');
5+
const EducationStudentProfile = require('../models/educationStudentProfile');
6+
7+
const normalizeToArray = (value) => {
8+
if (Array.isArray(value)) {
9+
return value;
10+
}
11+
12+
if (value === null || value === undefined) {
13+
return [];
14+
}
15+
16+
return [value];
17+
};
18+
19+
const buildManualStudentProgressResponse = (profile) => {
20+
const completedAtoms = [];
21+
const inProgressAtoms = [];
22+
const notStartedAtoms = [];
23+
24+
const subjects = Array.isArray(profile.subjects) ? profile.subjects : [];
25+
26+
subjects.forEach((subject) => {
27+
const subjectName = subject?.name || 'General';
28+
const molecules = Array.isArray(subject?.molecules) ? subject.molecules : [];
29+
30+
molecules.forEach((molecule, index) => {
31+
const status = molecule?.status || 'not_started';
32+
const fallbackId = `${subjectName}-${molecule?.label || molecule?.name || index}`
33+
.toLowerCase()
34+
.replace(/[^a-z0-9]+/g, '-');
35+
36+
const atomData = {
37+
atomId: molecule?.atomId || fallbackId,
38+
name: molecule?.label || molecule?.name || `Molecule ${index + 1}`,
39+
description: molecule?.description || '',
40+
difficulty: molecule?.difficulty || 'medium',
41+
moleculeType: molecule?.moleculeType || subjectName,
42+
subject: subjectName,
43+
status,
44+
grade: molecule?.grade || (status === 'completed' ? 'A' : 'pending'),
45+
timestamp:
46+
molecule?.completedAt ||
47+
molecule?.startedAt ||
48+
profile?.updatedAt ||
49+
profile?.createdAt ||
50+
null,
51+
sourceTask: molecule?.sourceTask
52+
? {
53+
reference: molecule.sourceTask,
54+
taskType: molecule?.taskType || null,
55+
lessonPlan: molecule?.lessonPlan || null,
56+
assignedAt: molecule?.assignedAt || null,
57+
completedAt: molecule?.completedAt || null,
58+
}
59+
: null,
60+
};
61+
62+
if (status === 'completed') {
63+
completedAtoms.push(atomData);
64+
} else if (status === 'in_progress') {
65+
inProgressAtoms.push(atomData);
66+
} else {
67+
notStartedAtoms.push(atomData);
68+
}
69+
});
70+
});
71+
72+
const summaryTotals = profile?.progressSummary || {};
73+
const totalAtomsFallback =
74+
completedAtoms.length + inProgressAtoms.length + notStartedAtoms.length;
75+
76+
return {
77+
student: {
78+
id: profile._id,
79+
firstName: profile?.firstName || (profile?.name ? profile.name.split(' ')[0] : 'Student'),
80+
lastName:
81+
profile?.lastName || (profile?.name ? profile.name.split(' ').slice(1).join(' ') : ''),
82+
email: profile?.email || '',
83+
profilePic: profile?.avatarUrl || null,
84+
location: profile?.location || '',
85+
educationProfile: {
86+
learningLevel: profile?.gradeLevel || profile?.learningLevel || null,
87+
strengths: normalizeToArray(
88+
profile?.strengths ||
89+
profile?.educationProfile?.student?.strengths ||
90+
profile?.student?.strengths,
91+
),
92+
challengingAreas: normalizeToArray(
93+
profile?.challengingAreas ||
94+
profile?.educationProfile?.student?.challengingAreas ||
95+
profile?.student?.challengingAreas,
96+
),
97+
},
98+
},
99+
progress: {
100+
completed: completedAtoms,
101+
inProgress: inProgressAtoms,
102+
notStarted: notStartedAtoms,
103+
},
104+
summary: {
105+
totalCompleted: summaryTotals.totalCompleted ?? completedAtoms.length,
106+
totalInProgress: summaryTotals.totalInProgress ?? inProgressAtoms.length,
107+
totalNotStarted: summaryTotals.totalNotStarted ?? notStartedAtoms.length,
108+
totalAtoms: summaryTotals.totalAtoms ?? totalAtomsFallback,
109+
},
110+
};
111+
};
5112

6113
const progressController = function () {
7114
// Get all progress records
@@ -86,29 +193,33 @@ const progressController = function () {
86193
try {
87194
const { studentId, atomId, status, grade, feedback } = req.body;
88195

196+
// Sanitize inputs to prevent NoSQL injection
197+
const sanitizedStudentId = String(studentId);
198+
const sanitizedAtomId = String(atomId);
199+
89200
// Validate student exists
90-
const student = await UserProfile.findById(studentId);
201+
const student = await UserProfile.findById(sanitizedStudentId);
91202
if (!student) {
92203
return res.status(404).json({ error: 'Student not found' });
93204
}
94205

95206
// Validate atom exists
96-
const atom = await Atom.findById(atomId);
207+
const atom = await Atom.findById(sanitizedAtomId);
97208
if (!atom) {
98209
return res.status(404).json({ error: 'Atom not found' });
99210
}
100211

101212
// Check if progress record already exists
102-
const existingProgress = await Progress.findOne({ studentId, atomId });
213+
const existingProgress = await Progress.findOne({ studentId: sanitizedStudentId, atomId: sanitizedAtomId });
103214
if (existingProgress) {
104215
return res
105216
.status(400)
106217
.json({ error: 'Progress record already exists for this student and atom' });
107218
}
108219

109220
const progress = new Progress({
110-
studentId,
111-
atomId,
221+
studentId: sanitizedStudentId,
222+
atomId: sanitizedAtomId,
112223
status: status || 'not_started',
113224
grade: grade || 'pending',
114225
feedback,
@@ -324,6 +435,156 @@ const progressController = function () {
324435
}
325436
};
326437

438+
// Get educator view of student progress with molecules (atoms)
439+
const getEducatorStudentProgress = async (req, res) => {
440+
try {
441+
const { studentId } = req.params;
442+
443+
let manualProfile = null;
444+
if (mongoose.Types.ObjectId.isValid(studentId)) {
445+
manualProfile = await EducationStudentProfile.findById(studentId).lean();
446+
}
447+
448+
if (manualProfile) {
449+
const response = buildManualStudentProgressResponse(manualProfile);
450+
return res.status(200).json(response);
451+
}
452+
453+
const EducationTask = require('../models/educationTask');
454+
455+
// Validate student exists
456+
const student = await UserProfile.findById(studentId).select(
457+
'firstName lastName email educationProfiles profilePic location',
458+
);
459+
460+
if (!student) {
461+
return res.status(404).json({ error: 'Student not found' });
462+
}
463+
464+
// Get all atoms and their progress for this student
465+
const progressRecords = await Progress.find({ studentId })
466+
.populate({
467+
path: 'atomId',
468+
select: 'name description difficulty subjectId moleculeType',
469+
populate: {
470+
path: 'subjectId',
471+
select: 'name',
472+
},
473+
})
474+
.sort({ updatedAt: -1 });
475+
476+
// Get all tasks for this student to find source task info
477+
const tasks = await EducationTask.find({ studentId })
478+
.populate('lessonPlanId', 'title theme')
479+
.populate('atomIds', 'name');
480+
481+
// Create a map of atomId to task info
482+
const atomToTaskMap = {};
483+
tasks.forEach((task) => {
484+
if (task.atomIds && task.atomIds.length > 0) {
485+
task.atomIds.forEach((atom) => {
486+
if (!atomToTaskMap[atom._id]) {
487+
atomToTaskMap[atom._id] = {
488+
taskId: task._id,
489+
taskType: task.type,
490+
lessonPlan: task.lessonPlanId ? task.lessonPlanId.title : null,
491+
assignedAt: task.assignedAt,
492+
completedAt: task.completedAt,
493+
};
494+
}
495+
});
496+
}
497+
});
498+
499+
// Categorize atoms by status
500+
const completedAtoms = [];
501+
const inProgressAtoms = [];
502+
const notStartedAtoms = [];
503+
504+
progressRecords.forEach((progress) => {
505+
if (!progress.atomId) return;
506+
507+
const atomData = {
508+
atomId: progress.atomId._id,
509+
name: progress.atomId.name,
510+
description: progress.atomId.description,
511+
difficulty: progress.atomId.difficulty,
512+
moleculeType: progress.atomId.moleculeType,
513+
subject: progress.atomId.subjectId ? progress.atomId.subjectId.name : null,
514+
status: progress.status,
515+
grade: progress.grade,
516+
timestamp: progress.updatedAt,
517+
sourceTask: atomToTaskMap[progress.atomId._id] || null,
518+
};
519+
520+
if (progress.status === 'completed') {
521+
completedAtoms.push(atomData);
522+
} else if (progress.status === 'in_progress') {
523+
inProgressAtoms.push(atomData);
524+
} else {
525+
notStartedAtoms.push(atomData);
526+
}
527+
});
528+
529+
// Get all atoms to show unearned ones
530+
const allAtoms = await Atom.find({})
531+
.populate('subjectId', 'name')
532+
.select('name description difficulty moleculeType subjectId');
533+
534+
// Find unearned atoms (atoms not in progress records)
535+
const progressAtomIds = new Set(progressRecords.map((p) => p.atomId?._id.toString()).filter(Boolean));
536+
const unearnedAtoms = allAtoms
537+
.filter((atom) => !progressAtomIds.has(atom._id.toString()))
538+
.map((atom) => ({
539+
atomId: atom._id,
540+
name: atom.name,
541+
description: atom.description,
542+
difficulty: atom.difficulty,
543+
moleculeType: atom.moleculeType,
544+
subject: atom.subjectId ? atom.subjectId.name : null,
545+
status: 'not_started',
546+
grade: 'pending',
547+
timestamp: null,
548+
sourceTask: null,
549+
}));
550+
551+
const locationParts = [];
552+
if (student?.location?.city) {
553+
locationParts.push(student.location.city);
554+
}
555+
if (student?.location?.country) {
556+
locationParts.push(student.location.country);
557+
}
558+
559+
const response = {
560+
student: {
561+
id: student._id,
562+
firstName: student.firstName,
563+
lastName: student.lastName,
564+
email: student.email,
565+
profilePic: student.profilePic || null,
566+
location: locationParts.join(', '),
567+
educationProfile: student.educationProfiles?.student || null,
568+
},
569+
progress: {
570+
completed: completedAtoms,
571+
inProgress: inProgressAtoms,
572+
notStarted: notStartedAtoms.concat(unearnedAtoms),
573+
},
574+
summary: {
575+
totalCompleted: completedAtoms.length,
576+
totalInProgress: inProgressAtoms.length,
577+
totalNotStarted: notStartedAtoms.length + unearnedAtoms.length,
578+
totalAtoms: allAtoms.length,
579+
},
580+
};
581+
582+
res.status(200).json(response);
583+
} catch (error) {
584+
res.status(500).json({ error: error.message });
585+
}
586+
};
587+
327588
return {
328589
getProgress,
329590
getProgressByStudent,
@@ -337,6 +598,7 @@ const progressController = function () {
337598
gradeProgress,
338599
getProgressByStatus,
339600
getStudentProgressSummary,
601+
getEducatorStudentProgress,
340602
};
341603
};
342604

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const mongoose = require('mongoose');
2+
3+
const educationStudentProfileSchema = new mongoose.Schema(
4+
{},
5+
{
6+
collection: 'education_student_profiles',
7+
strict: false,
8+
timestamps: false,
9+
},
10+
);
11+
12+
module.exports = mongoose.model('EducationStudentProfile', educationStudentProfileSchema);

src/routes/progressRouter.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const express = require('express');
2+
3+
const router = express.Router();
4+
const progressController = require('../controllers/progressController')();
5+
6+
// Educator endpoint - get student progress with molecules visualization data
7+
router.get('/educator/student-progress/:studentId', progressController.getEducatorStudentProgress);
8+
9+
// Get all progress records
10+
router.get('/', progressController.getProgress);
11+
12+
// Get progress by student
13+
router.get('/student/:studentId', progressController.getProgressByStudent);
14+
15+
// Get progress by atom
16+
router.get('/atom/:atomId', progressController.getProgressByAtom);
17+
18+
// Get progress by status
19+
router.get('/status/:status', progressController.getProgressByStatus);
20+
21+
// Get student progress summary
22+
router.get('/summary/:studentId', progressController.getStudentProgressSummary);
23+
24+
// Get specific progress record
25+
router.get('/:id', progressController.getProgressById);
26+
27+
// Get progress by student and atom
28+
router.get('/student/:studentId/atom/:atomId', progressController.getProgressByStudentAndAtom);
29+
30+
// Create new progress record
31+
router.post('/', progressController.createProgress);
32+
33+
// Update progress
34+
router.patch('/:id', progressController.updateProgress);
35+
36+
// Delete progress
37+
router.delete('/:id', progressController.deleteProgress);
38+
39+
// Update progress status
40+
router.patch('/:id/status', progressController.updateProgressStatus);
41+
42+
// Grade progress
43+
router.patch('/:id/grade', progressController.gradeProgress);
44+
45+
module.exports = router;

src/startup/routes.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const title = require('../models/title');
5454
const blueSquareEmailAssignment = require('../models/BlueSquareEmailAssignment');
5555
const hgnformRouter = require('../routes/hgnformRouter');
5656
const hgnFormResponseRouter = require('../routes/hgnFormResponseRouter');
57+
const progressRouter = require('../routes/progressRouter');
5758

5859
const questionnaireAnalyticsRouter = require('../routes/questionnaireAnalyticsRouter');
5960
const applicantAnalyticsRouter = require('../routes/applicantAnalyticsRoutes');
@@ -475,6 +476,7 @@ module.exports = function (app) {
475476
app.use('/api/questions', hgnformRouter);
476477
app.use('/api/issues', bmIssuesRouter);
477478
app.use('/api/hgnform', hgnFormResponseRouter);
479+
app.use('/api/progress', progressRouter);
478480
app.use('/api/skills', userSkillTabsRouter);
479481
app.use('/api/questionnaire-analytics/', questionnaireAnalyticsRouter);
480482
app.use('/api/applicant-analytics/', applicantAnalyticsRouter);

0 commit comments

Comments
 (0)