Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
44b17ed
Merge pull request #1120 from OneCommunityGlobal/development
Shreyav2000 Sep 27, 2024
ab610cd
code for carrying over additional hours to next week for core team me…
Oct 30, 2024
0aa9302
Jest setup
ArtemisNyx3 Jul 5, 2025
67d9256
starting from scratch
ArtemisNyx3 Jul 18, 2025
43ab008
initial structure for describe
ArtemisNyx3 Jul 18, 2025
c86d861
desdcribed testcases and divivded them into 2 describes
ArtemisNyx3 Jul 18, 2025
c8c625e
added todo and cleaned up dependencies
ArtemisNyx3 Jul 18, 2025
607ab84
mocked mongo server
ArtemisNyx3 Jul 18, 2025
c760f0a
Base test completed <3
ArtemisNyx3 Jul 19, 2025
a7b74e0
fixed testcase to run in mock DB
ArtemisNyx3 Jul 20, 2025
cd2c2ef
fixed mock timeentires to actually be counted
ArtemisNyx3 Jul 20, 2025
42be398
test case 2 created
ArtemisNyx3 Jul 20, 2025
1267922
add tests written
ArtemisNyx3 Jul 20, 2025
0a7362c
added more tests
ArtemisNyx3 Jul 26, 2025
4ec352d
Merge branch 'development' into Nikita-Core-Team-member’s-additional-…
ArtemisNyx3 Aug 30, 2025
02ff47f
Merge branch 'development' into Nikita-Core-Team-member’s-additional-…
ArtemisNyx3 Sep 8, 2025
f70c914
Merge branch 'development' into Nikita-Core-Team-member’s-additional-…
ArtemisNyx3 Nov 2, 2025
11a7908
mocked timezone
ArtemisNyx3 Nov 8, 2025
e3d36d1
Merge branch 'development' into Nikita-Core-Team-member’s-additional-…
ArtemisNyx3 Nov 16, 2025
342fa58
fixed issues with test data
ArtemisNyx3 Nov 16, 2025
a53c221
refactored clunky user creation code to use builder design pattern
ArtemisNyx3 Nov 23, 2025
3469c51
timeentry builder class
ArtemisNyx3 Nov 25, 2025
26df49c
refacatored Edge cases to use the new builder classes
ArtemisNyx3 Nov 25, 2025
52cc6b9
feat: Add lesson plan submission route with file upload support
ArtemisNyx3 Dec 23, 2025
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
109 changes: 70 additions & 39 deletions src/controllers/educationPortal/browsableLessonPlansController.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
const mongoose = require('mongoose');

const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfile) {
const ALLOWED_SORT_FIELDS = new Set(['createdAt', 'updatedAt', 'title', 'difficulty', 'popularity']);
const ALLOWED_SORT_FIELDS = new Set([
'createdAt',
'updatedAt',
'title',
'difficulty',
'popularity',
]);

function parseArrayParam(val) {
if (!val) return undefined;
if (Array.isArray(val)) return val;
return String(val).split(',').map((s) => s.trim()).filter(Boolean);
return String(val)
.split(',')
.map((s) => s.trim())
.filter(Boolean);
}

const getLessonPlans = async (req, res) => {
Expand All @@ -33,7 +42,7 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil
// Handle both singular and plural subject params
const subjectList = parseArrayParam(subjects || subject);
if (subjectList && subjectList.length) {
filter.subjects = { $in: subjectList.map(s => new RegExp(s, 'i')) };
filter.subjects = { $in: subjectList.map((s) => new RegExp(s, 'i')) };
}

if (difficulty) {
Expand All @@ -44,7 +53,7 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil
// Handle both singular and plural tag params
const tagList = parseArrayParam(tags || tag);
if (tagList && tagList.length) {
filter.tags = { $in: tagList.map(t => new RegExp(t, 'i')) };
filter.tags = { $in: tagList.map((t) => new RegExp(t, 'i')) };
}

if (author && mongoose.Types.ObjectId.isValid(author)) {
Expand All @@ -64,8 +73,10 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil

// Enhanced search with text index or regex fallback
if (search) {
const hasTextIndex = await BrowsableLessonPlan.collection.indexExists('title_text_description_text_content_text_tags_text');

const hasTextIndex = await BrowsableLessonPlan.collection.indexExists(
'title_text_description_text_content_text_tags_text',
);

if (hasTextIndex) {
filter.$text = { $search: search };
} else {
Expand All @@ -75,7 +86,7 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil
{ description: regex },
{ content: regex },
{ tags: regex },
{ subjects: regex }
{ subjects: regex },
];
}
}
Expand Down Expand Up @@ -136,22 +147,24 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil
});
} catch (error) {
console.error('Error getting lesson plans:', error);
return res.status(500).json({ success: false, error: 'Failed to fetch lesson plans', details: error.message });
return res
.status(500)
.json({ success: false, error: 'Failed to fetch lesson plans', details: error.message });
}
};

const getLessonPlanById = async (req, res) => {
try {
const { id } = req.params;

if (!mongoose.Types.ObjectId.isValid(id)) {
return res.status(400).json({ success: false, error: 'Invalid lesson plan ID' });
}

const lessonPlan = await BrowsableLessonPlan.findByIdAndUpdate(
id,
{ $inc: { views: 1 } },
{ new: true }
{ new: true },
)
.populate('author', 'firstName lastName profilePic email')
.lean();
Expand All @@ -170,21 +183,28 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil
const saveStudentInterest = async (req, res) => {
try {
const { studentId, lessonPlanId } = req.body;

if (!studentId || !lessonPlanId) {
return res.status(400).json({ success: false, error: 'studentId and lessonPlanId are required' });
return res
.status(400)
.json({ success: false, error: 'studentId and lessonPlanId are required' });
}

if (!mongoose.Types.ObjectId.isValid(studentId) || !mongoose.Types.ObjectId.isValid(lessonPlanId)) {

if (
!mongoose.Types.ObjectId.isValid(studentId) ||
!mongoose.Types.ObjectId.isValid(lessonPlanId)
) {
return res.status(400).json({ success: false, error: 'Invalid ID format' });
}

const lesson = await BrowsableLessonPlan.findByIdAndUpdate(
lessonPlanId,
{ $inc: { savedCount: 1 } },
{ new: true }
).select('_id title').lean();

{ new: true },
)
.select('_id title')
.lean();

if (!lesson) {
return res.status(404).json({ success: false, error: 'Lesson plan not found' });
}
Expand All @@ -198,7 +218,7 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil
.populate({
path: 'educationProfiles.student.savedInterests',
select: 'title description subjects difficulty tags thumbnail createdAt savedCount views',
populate: { path: 'author', select: 'firstName lastName' }
populate: { path: 'author', select: 'firstName lastName' },
});

if (!updated) {
Expand All @@ -212,27 +232,31 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil
});
} catch (error) {
console.error('Error saving student interest:', error);
return res.status(500).json({ success: false, error: 'Failed to save interest', details: error.message });
return res
.status(500)
.json({ success: false, error: 'Failed to save interest', details: error.message });
}
};

const removeStudentInterest = async (req, res) => {
try {
const { lessonPlanId } = req.params;
const studentId = req.query.studentId || req.body.studentId;

if (!studentId || !lessonPlanId) {
return res.status(400).json({ success: false, error: 'studentId and lessonPlanId are required' });
return res
.status(400)
.json({ success: false, error: 'studentId and lessonPlanId are required' });
}

if (!mongoose.Types.ObjectId.isValid(studentId) || !mongoose.Types.ObjectId.isValid(lessonPlanId)) {

if (
!mongoose.Types.ObjectId.isValid(studentId) ||
!mongoose.Types.ObjectId.isValid(lessonPlanId)
) {
return res.status(400).json({ success: false, error: 'Invalid ID format' });
}

await BrowsableLessonPlan.findByIdAndUpdate(
lessonPlanId,
{ $inc: { savedCount: -1 } }
);
await BrowsableLessonPlan.findByIdAndUpdate(lessonPlanId, { $inc: { savedCount: -1 } });

const updated = await UserProfile.findByIdAndUpdate(
studentId,
Expand All @@ -243,7 +267,7 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil
.populate({
path: 'educationProfiles.student.savedInterests',
select: 'title description subjects difficulty tags thumbnail createdAt savedCount views',
populate: { path: 'author', select: 'firstName lastName' }
populate: { path: 'author', select: 'firstName lastName' },
});

if (!updated) {
Expand All @@ -257,18 +281,20 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil
});
} catch (error) {
console.error('Error removing saved interest:', error);
return res.status(500).json({ success: false, error: 'Failed to remove interest', details: error.message });
return res
.status(500)
.json({ success: false, error: 'Failed to remove interest', details: error.message });
}
};

const getStudentSavedInterests = async (req, res) => {
try {
const studentId = req.query.studentId || req.headers.studentid || req.body.studentId;

if (!studentId) {
return res.status(400).json({ success: false, error: 'studentId is required' });
}

if (!mongoose.Types.ObjectId.isValid(studentId)) {
return res.status(400).json({ success: false, error: 'Invalid studentId format' });
}
Expand All @@ -277,8 +303,9 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil
.select('educationProfiles.student.savedInterests firstName lastName')
.populate({
path: 'educationProfiles.student.savedInterests',
select: 'title description subjects difficulty tags thumbnail createdAt updatedAt savedCount views',
populate: { path: 'author', select: 'firstName lastName profilePic' }
select:
'title description subjects difficulty tags thumbnail createdAt updatedAt savedCount views',
populate: { path: 'author', select: 'firstName lastName profilePic' },
})
.lean();

Expand All @@ -298,28 +325,32 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil
});
} catch (error) {
console.error('Error fetching saved interests:', error);
return res.status(500).json({ success: false, error: 'Failed to fetch saved interests', details: error.message });
return res
.status(500)
.json({ success: false, error: 'Failed to fetch saved interests', details: error.message });
}
};

const checkIfSaved = async (req, res) => {
try {
const { studentId, lessonPlanId } = req.query;

if (!studentId || !lessonPlanId) {
return res.status(400).json({ success: false, error: 'studentId and lessonPlanId are required' });
return res
.status(400)
.json({ success: false, error: 'studentId and lessonPlanId are required' });
}

const user = await UserProfile.findById(studentId)
.select('educationProfiles.student.savedInterests')
.lean();

if (!user) {
return res.status(404).json({ success: false, error: 'Student not found' });
}

const savedInterests = user.educationProfiles?.student?.savedInterests || [];
const isSaved = savedInterests.some(id => id.toString() === lessonPlanId);
const isSaved = savedInterests.some((id) => id.toString() === lessonPlanId);

return res.status(200).json({ success: true, isSaved });
} catch (error) {
Expand All @@ -338,4 +369,4 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil
};
};

module.exports = browsableLessonPlansController;
module.exports = browsableLessonPlansController;
18 changes: 15 additions & 3 deletions src/helpers/userHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -534,8 +534,6 @@ const userHelper = function () {
const assignBlueSquareForTimeNotMet = async () => {
try {
const currentFormattedDate = moment().tz('America/Los_Angeles').format();
moment.tz('America/Los_Angeles').startOf('day').toISOString();

logger.logInfo(
`Job for assigning blue square for commitment not met starting at ${currentFormattedDate}`,
);
Expand All @@ -544,7 +542,6 @@ const userHelper = function () {
.tz('America/Los_Angeles')
.startOf('week')
.subtract(1, 'week');

const pdtEndOfLastWeek = moment().tz('America/Los_Angeles').endOf('week').subtract(1, 'week');

const users = await userProfile.find(
Expand Down Expand Up @@ -1015,6 +1012,7 @@ const userHelper = function () {
}
};

// Function to calculate carry-forward hours for Core Team
const applyMissedHourForCoreTeam = async () => {
try {
const currentDate = moment().tz('America/Los_Angeles').format();
Expand Down Expand Up @@ -1137,6 +1135,20 @@ const userHelper = function () {
}
};

// Email body generator for Core Team notifications
const generateCoreTeamEmailBody = (
user,
infringement,
additionalHours,
) => `<p>Dear ${user.firstName},</p>
<p>You have received a new blue square for not meeting the required hours.</p>
<p>Infringement details:</p>
<p>${infringement.description}</p>
<p>Additional hours for next week: ${additionalHours} hours.</p>
<p>Thank you for your continued dedication.</p>
<p>Best regards,</p>
<p>One Community</p>`;

const deleteBlueSquareAfterYear = async () => {
const nowLA = moment().tz('America/Los_Angeles');

Expand Down
8 changes: 6 additions & 2 deletions src/models/educationPortal/browsableLessonPlanModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const BrowsableLessonPlanSchema = new mongoose.Schema(
materials: [{ type: String }],
metadata: { type: mongoose.Schema.Types.Mixed },
},
{ timestamps: true }
{ timestamps: true },
);

// Text index for search functionality
Expand All @@ -44,4 +44,8 @@ BrowsableLessonPlanSchema.index({ featured: 1, createdAt: -1 });
BrowsableLessonPlanSchema.index({ savedCount: -1 });
BrowsableLessonPlanSchema.index({ views: -1 });

module.exports = mongoose.model('BrowsableLessonPlan', BrowsableLessonPlanSchema, 'browsableLessonPlans');
module.exports = mongoose.model(
'BrowsableLessonPlan',
BrowsableLessonPlanSchema,
'browsableLessonPlans',
);
2 changes: 1 addition & 1 deletion src/routes/educationPortal/browsableLessonPlanRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ module.exports = function (BrowsableLessonPlanModel, UserProfileModel) {
router.get('/student/saved-interests/check', controller.checkIfSaved);

return router;
};
};
26 changes: 26 additions & 0 deletions src/test/factories/testTimeEntryBuilder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const TimeEntry = require('../../models/timeentry');

class TimeEntryBuilder {
constructor() {
this.data = [];
return this;

Check failure on line 6 in src/test/factories/testTimeEntryBuilder.js

View workflow job for this annotation

GitHub Actions / Lint Check

Unexpected return statement in constructor
}
addEntry(userId, dateOfWork, totalHours, isTangible = true, entryType = 'task') {

Check failure on line 8 in src/test/factories/testTimeEntryBuilder.js

View workflow job for this annotation

GitHub Actions / Lint Check

Expected blank line between class members
this.data.push({
personId: userId,
dateOfWork: dateOfWork,
isTangible: isTangible,
entryType: entryType,
totalSeconds: totalHours * 3600,
createdDateTime: new Date(),
isActive: true,
});
return this;
}
async buildAndSave() {

Check failure on line 20 in src/test/factories/testTimeEntryBuilder.js

View workflow job for this annotation

GitHub Actions / Lint Check

Expected blank line between class members
const entries = await TimeEntry.create(this.data);
return entries;
}
}

module.exports = TimeEntryBuilder;
Loading
Loading