Skip to content

Commit e607a7a

Browse files
Merge branch 'develop' into iss2223-tests
2 parents 79ddfdc + 10bc222 commit e607a7a

File tree

20 files changed

+905
-77
lines changed

20 files changed

+905
-77
lines changed

controllers/extensionRequests.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,22 @@ const getSelfExtensionRequests = async (req, res) => {
202202
* @param res {Object} - Express response object
203203
*/
204204
const updateExtensionRequest = async (req, res) => {
205+
const { dev } = req.query;
206+
const isDev = dev === "true";
205207
try {
206208
const extensionRequest = await extensionRequestsQuery.fetchExtensionRequest(req.params.id);
207209
if (!extensionRequest.extensionRequestData) {
208210
return res.boom.notFound("Extension Request not found");
209211
}
210212

213+
if (
214+
isDev &&
215+
!req.userData?.roles.super_user &&
216+
extensionRequest.extensionRequestData.status !== EXTENSION_REQUEST_STATUS.PENDING
217+
) {
218+
return res.boom.badRequest("Only pending extension request can be updated");
219+
}
220+
211221
if (req.body.assignee) {
212222
const { taskData: task } = await tasks.fetchTask(extensionRequest.extensionRequestData.taskId);
213223
if (task.assignee !== (await getUsername(req.body.assignee))) {

controllers/tasks.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { updateUserStatusOnTaskUpdate, updateStatusOnTaskCompletion } = require("
1313
const dataAccess = require("../services/dataAccessLayer");
1414
const { parseSearchQuery } = require("../utils/tasks");
1515
const { addTaskCreatedAtAndUpdatedAtFields } = require("../services/tasks");
16+
const tasksService = require("../services/tasks");
1617
const { RQLQueryParser } = require("../utils/RQLParser");
1718
const { getMissedProgressUpdatesUsers } = require("../models/discordactions");
1819
const { logType } = require("../constants/logs");
@@ -134,7 +135,19 @@ const fetchPaginatedTasks = async (query) => {
134135

135136
const fetchTasks = async (req, res) => {
136137
try {
137-
const { status, page, size, prev, next, q: queryString, assignee, title, userFeatureFlag } = req.query;
138+
const {
139+
status,
140+
page,
141+
size,
142+
prev,
143+
next,
144+
q: queryString,
145+
assignee,
146+
title,
147+
userFeatureFlag,
148+
orphaned,
149+
dev,
150+
} = req.query;
138151
const transformedQuery = transformQuery(status, size, page, assignee, title);
139152

140153
if (queryString !== undefined) {
@@ -159,6 +172,28 @@ const fetchTasks = async (req, res) => {
159172
});
160173
}
161174

175+
const isOrphaned = orphaned === "true";
176+
const isDev = dev === "true";
177+
if (isOrphaned) {
178+
if (!isDev) {
179+
return res.boom.notFound("Route not found");
180+
}
181+
try {
182+
const orphanedTasks = await tasksService.fetchOrphanedTasks();
183+
if (!orphanedTasks || orphanedTasks.length === 0) {
184+
return res.sendStatus(204);
185+
}
186+
const tasksWithRdsAssigneeInfo = await fetchTasksWithRdsAssigneeInfo(orphanedTasks);
187+
return res.status(200).json({
188+
message: "Orphan tasks fetched successfully",
189+
data: tasksWithRdsAssigneeInfo,
190+
});
191+
} catch (error) {
192+
logger.error("Error in getting tasks which were orphaned", error);
193+
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
194+
}
195+
}
196+
162197
const paginatedTasks = await fetchPaginatedTasks({ ...transformedQuery, prev, next, userFeatureFlag });
163198
return res.json({
164199
message: "Tasks returned successfully!",

controllers/users.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const { addLog } = require("../models/logs");
3030
const { getUserStatus } = require("../models/userStatus");
3131
const config = require("config");
3232
const { generateUniqueUsername } = require("../services/users");
33+
const userService = require("../services/users");
3334
const discordDeveloperRoleId = config.get("discordDeveloperRoleId");
3435

3536
const verifyUser = async (req, res) => {
@@ -191,6 +192,30 @@ const getUsers = async (req, res) => {
191192
}
192193
}
193194

195+
const isDeparted = req.query.departed === "true";
196+
197+
if (isDeparted) {
198+
if (!dev) {
199+
return res.boom.notFound("Route not found");
200+
}
201+
try {
202+
const result = await dataAccess.retrieveUsers({ query: req.query });
203+
const departedUsers = await userService.getUsersWithIncompleteTasks(result.users);
204+
if (departedUsers.length === 0) return res.status(204).send();
205+
return res.json({
206+
message: "Users with abandoned tasks fetched successfully",
207+
users: departedUsers,
208+
links: {
209+
next: result.nextId ? getPaginationLink(req.query, "next", result.nextId) : "",
210+
prev: result.prevId ? getPaginationLink(req.query, "prev", result.prevId) : "",
211+
},
212+
});
213+
} catch (error) {
214+
logger.error("Error when fetching users who abandoned tasks:", error);
215+
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
216+
}
217+
}
218+
194219
if (transformedQuery?.filterBy === OVERDUE_TASKS) {
195220
try {
196221
const tasksData = await getOverdueTasks(days);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const skipAuthorizeRolesUnderFF = (authorizeMiddleware) => {
2+
return (req, res, next) => {
3+
const { dev } = req.query;
4+
const isDev = dev === "true";
5+
if (isDev) {
6+
next();
7+
} else {
8+
authorizeMiddleware(req, res, next);
9+
}
10+
};
11+
};
12+
13+
module.exports = skipAuthorizeRolesUnderFF;

middlewares/validators/tasks.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ const getTasksValidator = async (req, res, next) => {
193193
return value;
194194
}, "Invalid query format"),
195195
userFeatureFlag: joi.string().optional(),
196+
orphaned: joi.boolean().optional(),
196197
});
197198

198199
try {

middlewares/validators/user.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ async function getUsers(req, res, next) {
200200
filterBy: joi.string().optional(),
201201
days: joi.string().optional(),
202202
dev: joi.string().optional(),
203+
departed: joi.string().optional(),
203204
roles: joi.optional().custom((value, helpers) => {
204205
if (value !== "member") {
205206
return helpers.message("only member role is supported");

models/tasks.js

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const userModel = firestore.collection("users");
44
const ItemModel = firestore.collection("itemTags");
55
const dependencyModel = firestore.collection("taskDependencies");
66
const userUtils = require("../utils/users");
7-
const { updateTaskStatusToDone } = require("../services/tasks");
87
const { chunks } = require("../utils/array");
98
const { DOCUMENT_WRITE_SIZE } = require("../constants/constants");
109
const { fromFirestoreData, toFirestoreData, buildTasks } = require("../utils/tasks");
@@ -24,6 +23,42 @@ const {
2423
const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING, OLD_COMPLETED } = TASK_STATUS_OLD;
2524
const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages");
2625

26+
/**
27+
* Update multiple tasks' status to DONE in one batch operation.
28+
* @param {Object[]} tasksData - Tasks data to update, must contain 'id' and 'status' fields.
29+
* @returns {Object} - Summary of the batch operation.
30+
* @property {number} totalUpdatedStatus - Number of tasks that has their status updated to DONE.
31+
* @property {number} totalOperationsFailed - Number of tasks that failed to update.
32+
* @property {string[]} updatedTaskDetails - IDs of tasks that has their status updated to DONE.
33+
* @property {string[]} failedTaskDetails - IDs of tasks that failed to update.
34+
*/
35+
const updateTaskStatusToDone = async (tasksData) => {
36+
const batch = firestore.batch();
37+
const tasksBatch = [];
38+
const summary = {
39+
totalUpdatedStatus: 0,
40+
totalOperationsFailed: 0,
41+
updatedTaskDetails: [],
42+
failedTaskDetails: [],
43+
};
44+
tasksData.forEach((task) => {
45+
const updateTaskData = { ...task, status: "DONE" };
46+
batch.update(tasksModel.doc(task.id), updateTaskData);
47+
tasksBatch.push(task.id);
48+
});
49+
try {
50+
await batch.commit();
51+
summary.totalUpdatedStatus += tasksData.length;
52+
summary.updatedTaskDetails = [...tasksBatch];
53+
return { ...summary };
54+
} catch (err) {
55+
logger.error("Firebase batch Operation Failed!");
56+
summary.totalOperationsFailed += tasksData.length;
57+
summary.failedTaskDetails = [...tasksBatch];
58+
return { ...summary };
59+
}
60+
};
61+
2762
/**
2863
* Adds and Updates tasks
2964
*
@@ -701,6 +736,33 @@ const markUnDoneTasksOfArchivedUsersBacklog = async (users) => {
701736
}
702737
};
703738

739+
/**
740+
* Fetches all incomplete tasks for given user IDs.
741+
*
742+
* @param {string[]} userIds - The IDs of the users to fetch incomplete tasks for.
743+
* @returns {Promise<firebase.firestore.QuerySnapshot>} - The query snapshot object.
744+
* @throws {Error} - Throws an error if the database query fails.
745+
*/
746+
const fetchIncompleteTasksByUserIds = async (userIds) => {
747+
const COMPLETED_STATUSES = [DONE, COMPLETED];
748+
749+
if (!userIds || userIds.length === 0) {
750+
return [];
751+
}
752+
try {
753+
const incompleteTasksQuery = await tasksModel.where("assigneeId", "in", userIds).get();
754+
755+
const incompleteTaskForUsers = incompleteTasksQuery.docs.filter(
756+
(task) => !COMPLETED_STATUSES.includes(task.data().status)
757+
);
758+
759+
return incompleteTaskForUsers;
760+
} catch (error) {
761+
logger.error("Error when fetching incomplete tasks for users:", error);
762+
throw error;
763+
}
764+
};
765+
704766
module.exports = {
705767
updateTask,
706768
fetchTasks,
@@ -720,4 +782,6 @@ module.exports = {
720782
updateTaskStatus,
721783
updateOrphanTasksStatus,
722784
markUnDoneTasksOfArchivedUsersBacklog,
785+
updateTaskStatusToDone,
786+
fetchIncompleteTasksByUserIds,
723787
};

models/users.js

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ const firestore = require("../utils/firestore");
88
const { fetchWallet, createWallet } = require("../models/wallets");
99
const { updateUserStatus } = require("../models/userStatus");
1010
const { arraysHaveCommonItem, chunks } = require("../utils/array");
11-
const { archiveUsers } = require("../services/users");
12-
const { ALLOWED_FILTER_PARAMS, FIRESTORE_IN_CLAUSE_SIZE } = require("../constants/users");
11+
const {
12+
ALLOWED_FILTER_PARAMS,
13+
FIRESTORE_IN_CLAUSE_SIZE,
14+
USERS_PATCH_HANDLER_SUCCESS_MESSAGES,
15+
USERS_PATCH_HANDLER_ERROR_MESSAGES,
16+
} = require("../constants/users");
1317
const { DOCUMENT_WRITE_SIZE } = require("../constants/constants");
1418
const { userState } = require("../constants/userStatus");
1519
const { BATCH_SIZE_IN_CLAUSE } = require("../constants/firebase");
@@ -27,6 +31,52 @@ const { formatUsername } = require("../utils/username");
2731
const { logType } = require("../constants/logs");
2832
const { addLog } = require("../services/logService");
2933

34+
/**
35+
* Archive users by setting the roles.archived field to true.
36+
* This function commits the write in batches to avoid reaching the maximum number of writes per batch.
37+
* @param {Array} usersData - An array of user objects with the following properties: id, first_name, last_name
38+
* @returns {Promise} - A promise that resolves with a summary object containing the number of users updated and failed, and an array of updated and failed user details.
39+
*/
40+
const archiveUsers = async (usersData) => {
41+
const batch = firestore.batch();
42+
const usersBatch = [];
43+
const summary = {
44+
totalUsersArchived: 0,
45+
totalOperationsFailed: 0,
46+
updatedUserDetails: [],
47+
failedUserDetails: [],
48+
};
49+
50+
usersData.forEach((user) => {
51+
const { id, first_name: firstName, last_name: lastName } = user;
52+
const updatedUserData = {
53+
...user,
54+
roles: {
55+
...user.roles,
56+
archived: true,
57+
},
58+
updated_at: Date.now(),
59+
};
60+
batch.update(userModel.doc(id), updatedUserData);
61+
usersBatch.push({ id, firstName, lastName });
62+
});
63+
64+
try {
65+
await batch.commit();
66+
summary.totalUsersArchived += usersData.length;
67+
summary.updatedUserDetails = [...usersBatch];
68+
return {
69+
message: USERS_PATCH_HANDLER_SUCCESS_MESSAGES.ARCHIVE_USERS.SUCCESSFULLY_COMPLETED_BATCH_UPDATES,
70+
...summary,
71+
};
72+
} catch (err) {
73+
logger.error("Firebase batch Operation Failed!");
74+
summary.totalOperationsFailed += usersData.length;
75+
summary.failedUserDetails = [...usersBatch];
76+
return { message: USERS_PATCH_HANDLER_ERROR_MESSAGES.ARCHIVE_USERS.BATCH_DATA_UPDATED_FAILED, ...summary };
77+
}
78+
};
79+
3080
/**
3181
* Adds or updates the user data
3282
*
@@ -190,11 +240,11 @@ const getSuggestedUsers = async (skill) => {
190240
*/
191241
const fetchPaginatedUsers = async (query) => {
192242
const isDevMode = query.dev === "true";
193-
194243
try {
195244
const size = parseInt(query.size) || 100;
196245
const doc = (query.next || query.prev) && (await userModel.doc(query.next || query.prev).get());
197246

247+
const isArchived = query.departed === "true";
198248
let dbQuery;
199249
/**
200250
* !!NOTE : At the time of writing we only support member in the role query
@@ -203,9 +253,9 @@ const fetchPaginatedUsers = async (query) => {
203253
* if you're making changes to this code remove the archived check in the role query, example: role=archived,member
204254
*/
205255
if (query.roles === "member") {
206-
dbQuery = userModel.where("roles.archived", "==", false).where("roles.member", "==", true);
256+
dbQuery = userModel.where("roles.archived", "==", isArchived).where("roles.member", "==", true);
207257
} else {
208-
dbQuery = userModel.where("roles.archived", "==", false).orderBy("username");
258+
dbQuery = userModel.where("roles.archived", "==", isArchived).orderBy("username");
209259
}
210260

211261
let compositeQuery = [dbQuery];
@@ -225,6 +275,10 @@ const fetchPaginatedUsers = async (query) => {
225275
}
226276

227277
if (Object.keys(query).length) {
278+
if (query.departed) {
279+
compositeQuery = compositeQuery.map((query) => query.where("roles.in_discord", "==", false));
280+
dbQuery = dbQuery.where("roles.in_discord", "==", false);
281+
}
228282
if (query.search) {
229283
const searchValue = query.search.toLowerCase().trim();
230284
dbQuery = dbQuery.startAt(searchValue).endAt(searchValue + "\uf8ff");
@@ -1038,7 +1092,26 @@ const updateUsersWithNewUsernames = async () => {
10381092
}
10391093
};
10401094

1095+
/**
1096+
* Fetches users who are not in the Discord server.
1097+
* @returns {Promise<FirebaseFirestore.QuerySnapshot>} - A promise that resolves to a Firestore QuerySnapshot containing the users matching the criteria.
1098+
* @throws {Error} - Throws an error if the database query fails.
1099+
*/
1100+
const fetchUsersNotInDiscordServer = async () => {
1101+
try {
1102+
const usersNotInDiscordServer = await userModel
1103+
.where("roles.archived", "==", true)
1104+
.where("roles.in_discord", "==", false)
1105+
.get();
1106+
return usersNotInDiscordServer;
1107+
} catch (error) {
1108+
logger.error(`Error in getting users who are not in discord server: ${error}`);
1109+
throw error;
1110+
}
1111+
};
1112+
10411113
module.exports = {
1114+
archiveUsers,
10421115
addOrUpdate,
10431116
fetchPaginatedUsers,
10441117
fetchUser,
@@ -1067,4 +1140,5 @@ module.exports = {
10671140
fetchUserForKeyValue,
10681141
getNonNickNameSyncedUsers,
10691142
updateUsersWithNewUsernames,
1143+
fetchUsersNotInDiscordServer,
10701144
};

routes/extensionRequests.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ const {
1010
updateExtensionRequestStatus,
1111
getExtensionRequestsValidator,
1212
} = require("../middlewares/validators/extensionRequests");
13+
const skipAuthorizeRolesUnderFF = require("../middlewares/skipAuthorizeRolesWrapper");
1314

1415
router.post("/", authenticate, createExtensionRequest, extensionRequests.createTaskExtensionRequest);
1516
router.get("/", authenticate, getExtensionRequestsValidator, extensionRequests.fetchExtensionRequests);
1617
router.get("/self", authenticate, extensionRequests.getSelfExtensionRequests);
1718
router.get("/:id", authenticate, authorizeRoles([SUPERUSER, APPOWNER]), extensionRequests.getExtensionRequest);
19+
// remove the skipAuthorizeRolesUnderFF & authorizeRoles middleware when removing the feature flag
1820
router.patch(
1921
"/:id",
2022
authenticate,
21-
authorizeRoles([SUPERUSER, APPOWNER]),
23+
skipAuthorizeRolesUnderFF(authorizeRoles([SUPERUSER, APPOWNER])),
2224
updateExtensionRequest,
2325
extensionRequests.updateExtensionRequest
2426
);

0 commit comments

Comments
 (0)