diff --git a/src/controllers/badgeController.js b/src/controllers/badgeController.js index 24d2debbb..316230ccb 100644 --- a/src/controllers/badgeController.js +++ b/src/controllers/badgeController.js @@ -43,11 +43,11 @@ const badgeController = function (Badge) { }; /** - * Updated Date: 12/06/2023 + * Updated Date: 01/12/2024 * Updated By: Shengwei * Function added: * - Added data validation for earned date and badge count mismatch. - * - Added fillEarnedDateToMatchCount function to resolve earned date and badge count mismatch. + * - Added fillEarnedDateToMatchCount function to resolve earned date and badge count mismatch. (Deleted due to new requirement) * - Refactored data validation for duplicate badge id. * - Added data validation for badge count should greater than 0. * - Added formatDate function to format date to MMM-DD-YY. @@ -58,14 +58,6 @@ const badgeController = function (Badge) { return moment(currentDate).tz('America/Los_Angeles').format('MMM-DD-YY'); }; - const fillEarnedDateToMatchCount = (earnedDate, count) => { - const result = [...earnedDate]; - while (result.length < count) { - result.push(formatDate()); - } - return result; - }; - const assignBadges = async function (req, res) { if (!(await hasPermission(req.body.requestor, 'assignBadges'))) { res.status(403).send('You are not authorized to assign badges.'); @@ -88,32 +80,16 @@ const badgeController = function (Badge) { newBadgeCollection = req.body.badgeCollection.map((element) => { if (badgeCounts[element.badge]) { throw new Error('Duplicate badges sent in.'); - // res.status(500).send('Duplicate badges sent in.'); - // return; } badgeCounts[element.badge] = element.count; // Validation: count should be greater than 0 if (element.count < 1) { throw new Error('Badge count should be greater than 0.'); } - if (element.count !== element.earnedDate.length) { - element.earnedDate = fillEarnedDateToMatchCount( - element.earnedDate, - element.count, - ); - element.lastModified = Date.now(); - logger.logInfo( - `Badge count and earned dates mismatched found. ${Date.now()} was generated for user ${userToBeAssigned}. Badge record ID ${ - element._id - }; Badge Type ID ${element.badge}`, - ); - } - return element; }); } catch (err) { - res - .status(500) - .send(`Internal Error: Badge Collection. ${err.message}`); + logger.logException(`Internal Error: Badge Collection. ${err.message} User ID: ${userToBeAssigned} Badge Collection: ${JSON.stringify(req.body.badgeCollection)}`); + res.status(500).send(`Internal Error: Badge Collection. ${ err.message}`); return; } record.badgeCollection = newBadgeCollection; diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index 058d22254..761ce178b 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -985,18 +985,21 @@ const userHelper = function () { if (!recordToUpdate) { throw new Error('Badge not found'); } + // If the count is the same, do nothing + if (recordToUpdate.count === count) { + return; + } const copyOfEarnedDate = recordToUpdate.earnedDate; - if (copyOfEarnedDate.length < count) { - // if the EarnedDate count is less than the new count, add a earned date to the end of the collection - while (copyOfEarnedDate.length < count) { + // Update: We refrain from automatically correcting the mismatch problem as we intend to preserve the original + // earned date even when a badge is deleted. This approach ensures that a record of badges earned is maintained, + // preventing oversight of any mismatches caused by bugs. + if (recordToUpdate.count < count) { + let dateToAdd = count - recordToUpdate.count; + // if the EarnedDate count is less than the new count, add a earned date to the end of the collection + while (dateToAdd > 0) { copyOfEarnedDate.push(earnedDateBadge()); + dateToAdd -= 1; } - } else { - // if the EarnedDate count is greater than the new count, remove the oldest earned date of the collection until it matches the new count - 1 - while (copyOfEarnedDate.length >= count) { - copyOfEarnedDate.shift(); - } - copyOfEarnedDate.push(earnedDateBadge()); } newEarnedDate = [...copyOfEarnedDate]; userProfile.updateOne( @@ -1006,6 +1009,7 @@ const userHelper = function () { 'badgeCollection.$.count': count, 'badgeCollection.$.lastModified': Date.now().toString(), 'badgeCollection.$.earnedDate': newEarnedDate, + 'badgeCollection.$.hasBadgeDeletionImpact': recordToUpdate.count > count, // badge deletion impact set to true if the new count is less than the old count }, }, (err) => { diff --git a/src/models/userProfile.js b/src/models/userProfile.js index 075a33e9d..153028818 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -1,9 +1,9 @@ -const mongoose = require("mongoose"); -const moment = require("moment-timezone"); +const mongoose = require('mongoose'); +const moment = require('moment-timezone'); const { Schema } = mongoose; -const validate = require("mongoose-validator"); -const bcrypt = require("bcryptjs"); +const validate = require('mongoose-validator'); +const bcrypt = require('bcryptjs'); const SALT_ROUNDS = 10; const nextDay = new Date(); @@ -15,12 +15,11 @@ const userProfileSchema = new Schema({ required: true, validate: { validator(v) { - const passwordregex = - /(?=^.{8,}$)((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/; + const passwordregex = /(?=^.{8,}$)((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/; return passwordregex.test(v); }, message: - "{VALUE} is not a valid password!password should be at least 8 charcaters long with uppercase, lowercase and number/special char.", + '{VALUE} is not a valid password!password should be at least 8 charcaters long with uppercase, lowercase and number/special char.', }, }, isActive: { type: Boolean, required: true, default: true }, @@ -50,7 +49,7 @@ const userProfileSchema = new Schema({ required: true, unique: true, validate: [ - validate({ validator: "isEmail", message: "Email address is invalid" }), + validate({ validator: 'isEmail', message: 'Email address is invalid' }), ], }, copiedAiPrompt: { type: Date, default: Date.now() }, @@ -69,14 +68,18 @@ const userProfileSchema = new Schema({ { _id: Schema.Types.ObjectId, Name: String, Link: { type: String } }, ], adminLinks: [{ _id: Schema.Types.ObjectId, Name: String, Link: String }], - teams: [{ type: mongoose.SchemaTypes.ObjectId, ref: "team" }], - projects: [{ type: mongoose.SchemaTypes.ObjectId, ref: "project" }], + teams: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'team' }], + projects: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'project' }], badgeCollection: [ { - badge: { type: mongoose.SchemaTypes.ObjectId, ref: "badge" }, + badge: { type: mongoose.SchemaTypes.ObjectId, ref: 'badge' }, count: { type: Number, default: 0 }, earnedDate: { type: Array, default: [] }, - lastModified: { type: Date, required: true, default: new Date()}, + lastModified: { type: Date, required: true, default: Date.now() }, + // This field is used to determine if the badge deletion will impact the user's badge collection. + // If the user has a badge with hasBadgeDeletionImpact set to true, then the a mismatch in badge + // count and earned date will be intentionally created. + hasBadgeDeletionImpact: { type: Boolean, default: false }, featured: { type: Boolean, required: true, @@ -115,13 +118,13 @@ const userProfileSchema = new Schema({ }, ], location: { - userProvided: { type: String, default: "" }, + userProvided: { type: String, default: '' }, coords: { - lat: { type: Number, default: "" }, - lng: { type: Number, default: "" }, + lat: { type: Number, default: '' }, + lng: { type: Number, default: '' }, }, - country: { type: String, default: "" }, - city: { type: String, default: "" }, + country: { type: String, default: '' }, + city: { type: String, default: '' }, }, oldInfringements: [ { @@ -143,7 +146,7 @@ const userProfileSchema = new Schema({ dueDate: { type: Date, required: true, - default: moment().tz("America/Los_Angeles").endOf("week"), + default: moment().tz('America/Los_Angeles').endOf('week'), }, summary: { type: String }, uploadDate: { type: Date }, @@ -173,17 +176,17 @@ const userProfileSchema = new Schema({ category: { type: String, enum: [ - "Food", - "Energy", - "Housing", - "Education", - "Society", - "Economics", - "Stewardship", - "Other", - "Unspecified", + 'Food', + 'Energy', + 'Housing', + 'Education', + 'Society', + 'Economics', + 'Stewardship', + 'Other', + 'Unspecified', ], - default: "Other", + default: 'Other', }, hrs: { type: Number, default: 0 }, }, @@ -194,27 +197,27 @@ const userProfileSchema = new Schema({ date: { type: Date, required: true, - default: moment().tz("America/Los_Angeles").toDate(), + default: moment().tz('America/Los_Angeles').toDate(), }, initialSeconds: { type: Number, required: true }, newSeconds: { type: Number, required: true }, }, ], weeklySummaryNotReq: { type: Boolean, default: false }, - timeZone: { type: String, required: true, default: "America/Los_Angeles" }, + timeZone: { type: String, required: true, default: 'America/Los_Angeles' }, isVisible: { type: Boolean, default: false }, weeklySummaryOption: { type: String }, - bioPosted: { type: String, default: "default" }, + bioPosted: { type: String, default: 'default' }, isFirstTimelog: { type: Boolean, default: true }, teamCode: { type: String, - default: "", + default: '', validate: { validator(v) { const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$|^$/; return teamCoderegex.test(v); }, - message: "Please enter a code in the format of A-AAA or AAAAA", + message: 'Please enter a code in the format of A-AAA or AAAAA', }, }, infoCollections: [ @@ -229,22 +232,22 @@ const userProfileSchema = new Schema({ timeOffTill: { type: Date, default: undefined }, }); -userProfileSchema.pre("save", function (next) { +userProfileSchema.pre('save', function (next) { const user = this; - if (!user.isModified("password")) return next(); + if (!user.isModified('password')) return next(); return bcrypt .genSalt(SALT_ROUNDS) - .then((result) => bcrypt.hash(user.password, result)) + .then(result => bcrypt.hash(user.password, result)) .then((hash) => { user.password = hash; return next(); }) - .catch((error) => next(error)); + .catch(error => next(error)); }); module.exports = mongoose.model( - "userProfile", + 'userProfile', userProfileSchema, - "userProfiles" + 'userProfiles', );