Skip to content

Commit a880313

Browse files
dev to main sync (#1618)
1 parent 1e63004 commit a880313

25 files changed

+1583
-80
lines changed

constants/taskRequests.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
const TASK_REQUEST_STATUS = {
22
WAITING: "WAITING",
33
APPROVED: "APPROVED",
4+
PENDING: "PENDING",
5+
DENIED: "DENIED",
46
};
57

6-
module.exports = { TASK_REQUEST_STATUS };
8+
const TASK_REQUEST_TYPE = {
9+
ASSIGNMENT: "ASSIGNMENT",
10+
CREATION: "CREATION",
11+
};
12+
13+
module.exports = { TASK_REQUEST_STATUS, TASK_REQUEST_TYPE };

constants/tasks.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const TASK_STATUS_OLD = {
3030
OLD_PENDING: "pending",
3131
OLD_COMPLETED: "completed",
3232
};
33+
const DEFAULT_TASK_PRIORITY = "TBD";
3334

3435
const MAPPED_TASK_STATUS = {
3536
...TASK_STATUS,
@@ -39,4 +40,4 @@ const MAPPED_TASK_STATUS = {
3940

4041
const TASK_SIZE = 5;
4142

42-
module.exports = { TASK_TYPE, TASK_STATUS, TASK_STATUS_OLD, MAPPED_TASK_STATUS, TASK_SIZE };
43+
module.exports = { TASK_TYPE, TASK_STATUS, TASK_STATUS_OLD, MAPPED_TASK_STATUS, TASK_SIZE, DEFAULT_TASK_PRIORITY };

controllers/extensionRequests.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const { EXTENSION_REQUEST_STATUS } = require("../constants/extensionRequests");
66
const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages");
77
const { transformQuery } = require("../utils/extensionRequests");
88
const { parseQueryParams } = require("../utils/queryParser");
9+
const logsQuery = require("../models/logs");
10+
const { getFullName } = require("../utils/users");
911

1012
/**
1113
* Create ETA extension Request
@@ -232,6 +234,23 @@ const getSelfExtensionRequests = async (req, res) => {
232234
if (latestExtensionRequest && latestExtensionRequest.assigneeId !== userId) {
233235
allExtensionRequests = [];
234236
} else {
237+
// Add reviewer's name if status is not PENDING
238+
if (latestExtensionRequest.status === "APPROVED" || latestExtensionRequest.status === "DENIED") {
239+
const logs = await logsQuery.fetchLogs(
240+
{ "meta.extensionRequestId": latestExtensionRequest.id, limit: 1 },
241+
"extensionRequests"
242+
);
243+
244+
if (
245+
logs.length === 1 &&
246+
logs[0]?.meta?.userId &&
247+
(logs[0]?.body?.status === "APPROVED" || logs[0]?.body?.status === "DENIED") // Make sure log is only related to status change
248+
) {
249+
const superUserId = logs[0].meta.userId;
250+
const name = await getFullName(superUserId);
251+
latestExtensionRequest.reviewedBy = `${name?.first_name} ${name?.last_name}`;
252+
}
253+
}
235254
allExtensionRequests = [latestExtensionRequest];
236255
}
237256
} else {
@@ -270,6 +289,7 @@ const getSelfExtensionRequests = async (req, res) => {
270289
*/
271290
const updateExtensionRequest = async (req, res) => {
272291
try {
292+
const { dev = "false" } = req.query;
273293
const extensionRequest = await extensionRequestsQuery.fetchExtensionRequest(req.params.id);
274294
if (!extensionRequest.extensionRequestData) {
275295
return res.boom.notFound("Extension Request not found");
@@ -282,7 +302,39 @@ const updateExtensionRequest = async (req, res) => {
282302
}
283303
}
284304

285-
await extensionRequestsQuery.updateExtensionRequest(req.body, req.params.id);
305+
const promises = [extensionRequestsQuery.updateExtensionRequest(req.body, req.params.id)];
306+
// If flag is present, then only create log for change in ETA/reason by SU
307+
if (dev === "true") {
308+
let body = {};
309+
// Check if reason has been changed
310+
if (req.body.reason && req.body.reason !== extensionRequest.extensionRequestData.reason) {
311+
body = { ...body, oldReason: extensionRequest.extensionRequestData.reason, newReason: req.body.reason };
312+
}
313+
// Check if newEndsOn has been changed
314+
if (req.body.newEndsOn && req.body.newEndsOn !== extensionRequest.extensionRequestData.newEndsOn) {
315+
body = { ...body, oldEndsOn: extensionRequest.extensionRequestData.newEndsOn, newEndsOn: req.body.newEndsOn };
316+
}
317+
// Check if title has been changed
318+
if (req.body.title && req.body.title !== extensionRequest.extensionRequestData.title) {
319+
body = { ...body, oldTitle: extensionRequest.extensionRequestData.title, newTitle: req.body.title };
320+
}
321+
322+
// Validate if there's any update that actually happened, then only create the log
323+
if (Object.keys(body).length > 0) {
324+
const extensionLog = {
325+
type: "extensionRequests",
326+
meta: {
327+
extensionRequestId: req.params.id,
328+
taskId: extensionRequest.extensionRequestData.taskId,
329+
userId: req.userData.id,
330+
},
331+
body,
332+
};
333+
promises.push(addLog(extensionLog.type, extensionLog.meta, extensionLog.body));
334+
}
335+
}
336+
await Promise.all(promises);
337+
286338
return res.status(204).send();
287339
} catch (err) {
288340
logger.error(`Error while updating extension request: ${err}`);
@@ -298,6 +350,7 @@ const updateExtensionRequest = async (req, res) => {
298350
*/
299351
const updateExtensionRequestStatus = async (req, res) => {
300352
try {
353+
const { dev = "false" } = req.query;
301354
const extensionRequest = await extensionRequestsQuery.fetchExtensionRequest(req.params.id);
302355
if (!extensionRequest.extensionRequestData) {
303356
return res.boom.notFound("Extension Request not found");
@@ -307,6 +360,7 @@ const updateExtensionRequestStatus = async (req, res) => {
307360
const extensionLog = {
308361
type: "extensionRequests",
309362
meta: {
363+
...(dev === "true" && { extensionRequestId: req.params.id }), // if flag is present, add extensionRequestId
310364
taskId: extensionRequest.extensionRequestData.taskId,
311365
username: req.userData.username,
312366
userId: req.userData.id,

controllers/tasksRequests.js

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
const { INTERNAL_SERVER_ERROR, SOMETHING_WENT_WRONG } = require("../constants/errorMessages");
2+
const { TASK_REQUEST_TYPE } = require("../constants/taskRequests");
23
const taskRequestsModel = require("../models/taskRequests");
34
const tasksModel = require("../models/tasks.js");
5+
const githubService = require("../services/githubService");
6+
const usersUtils = require("../utils/users");
47

58
const fetchTaskRequests = async (_, res) => {
69
try {
7-
const data = await taskRequestsModel.fetchTaskRequests();
10+
const { dev } = _.query;
11+
const data = await taskRequestsModel.fetchTaskRequests(dev === "true");
812

913
if (data.length > 0) {
1014
return res.status(200).json({
@@ -43,6 +47,73 @@ const fetchTaskRequestById = async (req, res) => {
4347
}
4448
};
4549

50+
const addTaskRequests = async (req, res) => {
51+
try {
52+
const taskRequestData = req.body;
53+
const usernamePromise = usersUtils.getUsername(taskRequestData.userId);
54+
if (req.userData.id !== taskRequestData.userId && !req.userData.roles?.super_user) {
55+
return res.boom.forbidden("Not authorized to create the request");
56+
}
57+
if (taskRequestData.proposedDeadline < taskRequestData.proposedStartDate) {
58+
return res.boom.badRequest("Task deadline cannot be before the start date");
59+
}
60+
switch (taskRequestData.requestType) {
61+
case TASK_REQUEST_TYPE.ASSIGNMENT: {
62+
const taskDataPromise = tasksModel.fetchTask(taskRequestData.taskId);
63+
64+
const [{ taskData }, username] = await Promise.all([taskDataPromise, usernamePromise]);
65+
taskRequestData.taskTitle = taskData?.title;
66+
if (!username) {
67+
return res.boom.badRequest("User not found");
68+
}
69+
if (!taskData) {
70+
return res.boom.badRequest("Task does not exist");
71+
}
72+
break;
73+
}
74+
case TASK_REQUEST_TYPE.CREATION: {
75+
let issuePromise;
76+
try {
77+
const url = new URL(taskRequestData.externalIssueUrl);
78+
const issueUrlPaths = url.pathname.split("/");
79+
const repositoryName = issueUrlPaths[3];
80+
const issueNumber = issueUrlPaths[5];
81+
issuePromise = githubService.fetchIssuesById(repositoryName, issueNumber);
82+
} catch (error) {
83+
return res.boom.badRequest("External issue url is not valid");
84+
}
85+
const [issueData, username] = await Promise.all([issuePromise, usernamePromise]);
86+
taskRequestData.taskTitle = issueData?.title;
87+
if (!username) {
88+
return res.boom.badRequest("User not found");
89+
}
90+
if (!issueData) {
91+
return res.boom.badRequest("Issue does not exist");
92+
}
93+
break;
94+
}
95+
}
96+
const newTaskRequest = await taskRequestsModel.createRequest(taskRequestData, req.userData.username);
97+
if (newTaskRequest.isCreationRequestApproved) {
98+
return res.boom.conflict("Task exists for the given issue.");
99+
}
100+
if (newTaskRequest.alreadyRequesting) {
101+
return res.boom.badRequest("Task was already requested");
102+
}
103+
const statusCode = newTaskRequest.isCreate ? 201 : 200;
104+
return res.status(statusCode).json({
105+
message: "Task request successful.",
106+
data: {
107+
id: newTaskRequest.id,
108+
...newTaskRequest.taskRequest,
109+
},
110+
});
111+
} catch (err) {
112+
logger.error("Error while creating task request");
113+
return res.boom.serverUnavailable(SOMETHING_WENT_WRONG);
114+
}
115+
};
116+
46117
const addOrUpdate = async (req, res) => {
47118
try {
48119
const { taskId, userId } = req.body;
@@ -87,6 +158,15 @@ const approveTaskRequest = async (req, res) => {
87158

88159
const response = await taskRequestsModel.approveTaskRequest(taskRequestId, user);
89160

161+
if (response.taskRequestNotFound) {
162+
return res.boom.badRequest("Task request not found.");
163+
}
164+
if (response.isUserInvalid) {
165+
return res.boom.badRequest("User request not available.");
166+
}
167+
if (response.isTaskRequestInvalid) {
168+
return res.boom.badRequest("Task request was previously approved or rejected.");
169+
}
90170
return res.status(200).json({
91171
message: `Task successfully assigned to user ${response.approvedTo}`,
92172
taskRequest: response.taskRequest,
@@ -102,4 +182,5 @@ module.exports = {
102182
addOrUpdate,
103183
fetchTaskRequests,
104184
fetchTaskRequestById,
185+
addTaskRequests,
105186
};

controllers/users.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,17 @@ const getUserById = async (req, res) => {
8080
const getUsers = async (req, res) => {
8181
try {
8282
// getting user details by id if present.
83-
const query = req.query?.query ?? "";
84-
const transformedQuery = parseSearchQuery(query);
85-
const qualifiers = getQualifiers(query);
86-
83+
const { q, dev: devParam, query } = req.query;
84+
const dev = devParam === "true";
85+
const queryString = (dev ? q : query) || "";
86+
const transformedQuery = parseSearchQuery(queryString);
87+
const qualifiers = getQualifiers(queryString);
88+
// Should throw an error if the new query parameter is without feature flag
89+
if (q && !dev) {
90+
return res.boom.notFound("Route not found");
91+
}
8792
// getting user details by id if present.
93+
8894
if (req.query.id) {
8995
const id = req.query.id;
9096
let result, user;
@@ -135,7 +141,6 @@ const getUsers = async (req, res) => {
135141
// getting user details by discord id if present.
136142
const discordId = req.query.discordId;
137143

138-
const dev = req.query.dev === "true";
139144
if (req.query.discordId) {
140145
if (dev) {
141146
let result, user;

middlewares/taskRequests.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ async function validateUser(req, res, next) {
2424
if (!userStatusExists) {
2525
return res.boom.conflict("User status does not exist");
2626
}
27-
if (userStatus.currentStatus.state === userState.OOO) {
28-
return res.boom.conflict("User is currently OOO");
29-
}
3027
if (userStatus.currentStatus.state === userState.ACTIVE) {
3128
return res.boom.conflict("User is currently active on another task");
3229
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const joi = require("joi");
2+
const { TASK_REQUEST_TYPE } = require("../../constants/taskRequests");
3+
const githubOrg = config.get("githubApi.org");
4+
const githubBaseUrl = config.get("githubApi.baseUrl");
5+
const githubIssuerUrlPattern = new RegExp(`^${githubBaseUrl}/repos/${githubOrg}/.+/issues/\\d+$`);
6+
7+
const postTaskRequests = async (req, res, next) => {
8+
const taskAssignmentSchema = joi
9+
.object()
10+
.strict()
11+
.keys({
12+
taskId: joi.string().required(),
13+
externalIssueUrl: joi.string().regex(githubIssuerUrlPattern).optional(),
14+
requestType: joi.string().valid(TASK_REQUEST_TYPE.ASSIGNMENT).required(),
15+
userId: joi.string().required(),
16+
proposedStartDate: joi.number().required(),
17+
proposedDeadline: joi.number().required(),
18+
description: joi.string().optional(),
19+
});
20+
21+
const taskCreationSchema = joi
22+
.object()
23+
.strict()
24+
.keys({
25+
externalIssueUrl: joi.string().regex(githubIssuerUrlPattern).required(),
26+
requestType: joi.string().valid(TASK_REQUEST_TYPE.CREATION).required(),
27+
userId: joi.string().required(),
28+
proposedStartDate: joi.number().required(),
29+
proposedDeadline: joi.number().required(),
30+
description: joi.string().optional(),
31+
});
32+
const schema = joi.alternatives().try(taskAssignmentSchema, taskCreationSchema);
33+
34+
try {
35+
await schema.validateAsync(req.body);
36+
next();
37+
} catch (error) {
38+
logger.error(`Error validating postTaskRequests payload : ${error}`);
39+
res.boom.badRequest(error.details[0].context.message);
40+
}
41+
};
42+
43+
module.exports = {
44+
postTaskRequests,
45+
};

middlewares/validators/user.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ async function getUsers(req, res, next) {
189189
"string.empty": "prev value cannot be empty",
190190
}),
191191
query: joi.string().optional(),
192+
q: joi.string().optional(),
192193
filterBy: joi.string().optional(),
193194
days: joi.string().optional(),
194195
dev: joi.string().optional(),

models/logs.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const logsModel = firestore.collection("logs");
44
const admin = require("firebase-admin");
55
const { logType } = require("../constants/logs");
66
const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages");
7+
const { getFullName } = require("../utils/users");
78

89
/**
910
* Adds log
@@ -35,15 +36,17 @@ const addLog = async (type, meta, body) => {
3536
*/
3637
const fetchLogs = async (query, param) => {
3738
try {
39+
const { dev, ...remainingQuery } = query;
3840
let call = logsModel.where("type", "==", param);
39-
Object.keys(query).forEach((key) => {
41+
Object.keys(remainingQuery).forEach((key) => {
4042
// eslint-disable-next-line security/detect-object-injection
4143
if (key !== "limit" && key !== "lastDocId") {
42-
call = call.where(key, "==", query[key]);
44+
// eslint-disable-next-line security/detect-object-injection
45+
call = call.where(key, "==", remainingQuery[key]);
4346
}
4447
});
4548

46-
const { limit, lastDocId, userId } = query;
49+
const { limit, lastDocId, userId } = remainingQuery;
4750
let lastDoc;
4851
const limitDocuments = Number(limit);
4952

@@ -75,6 +78,22 @@ const fetchLogs = async (query, param) => {
7578
...doc.data(),
7679
});
7780
});
81+
82+
// If dev flag is presend and extensionRequest logs are requested, populate userId
83+
if (dev === "true" && param === "extensionRequests") {
84+
const userIdNameMap = {};
85+
for await (const log of logs) {
86+
if (log.meta.userId) {
87+
if (userIdNameMap[log.meta.userId]) {
88+
log.meta.name = userIdNameMap[log.meta.userId];
89+
} else {
90+
const name = await getFullName(log.meta.userId);
91+
log.meta.name = `${name?.first_name} ${name?.last_name}`;
92+
userIdNameMap[log.meta.userId] = log.meta.name;
93+
}
94+
}
95+
}
96+
}
7897
return logs;
7998
} catch (err) {
8099
logger.error("Error in adding log", err);

0 commit comments

Comments
 (0)