Skip to content

Commit a032680

Browse files
author
liuliquan
authored
feat: add skills to challenge, and search via skills (#678) (#682)
1 parent 528baf7 commit a032680

File tree

8 files changed

+250
-38
lines changed

8 files changed

+250
-38
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ workflows:
9090
branches:
9191
only:
9292
- dev
93-
- PLAT-3368
93+
- PLAT-3614
9494

9595
- "build-qa":
9696
context: org-global

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,14 @@
4242
"dependencies": {
4343
"@grpc/grpc-js": "^1.8.12",
4444
"@opensearch-project/opensearch": "^2.2.0",
45-
"@topcoder-framework/domain-challenge": "^0.23.0",
46-
"@topcoder-framework/lib-common": "^0.23.0",
45+
"@topcoder-framework/domain-challenge": "^v0.23.1-PLAT-3614.0",
46+
"@topcoder-framework/lib-common": "^v0.23.1-PLAT-3614.0",
4747
"aws-sdk": "^2.1145.0",
4848
"axios": "^0.19.0",
4949
"axios-retry": "^3.4.0",
5050
"bluebird": "^3.5.1",
5151
"body-parser": "^1.15.1",
52+
"compare-versions": "^6.1.0",
5253
"config": "^3.0.1",
5354
"cors": "^2.8.5",
5455
"decimal.js": "^10.4.3",

src/common/challenge-helper.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,55 @@ class ChallengeHelper {
9999
await Promise.all(promises);
100100
}
101101

102+
/**
103+
* Validate Challenge skills.
104+
* @param {Object} challenge the challenge
105+
* @param {oldChallenge} challenge the old challenge data
106+
*/
107+
async validateSkills(challenge, oldChallenge) {
108+
if (!challenge.skills) {
109+
return;
110+
}
111+
112+
const ids = _.uniq(_.map(challenge.skills, "id"));
113+
114+
if (oldChallenge && oldChallenge.status === constants.challengeStatuses.Completed) {
115+
// Don't allow edit skills for Completed challenges
116+
if (!_.isEqual(ids, _.uniq(_.map(oldChallenge.skills, "id")))) {
117+
throw new errors.BadRequestError("Cannot update skills for challenges with Completed status");
118+
}
119+
}
120+
121+
if (!ids.length) {
122+
return;
123+
}
124+
125+
const standSkills = await helper.getStandSkills(ids);
126+
127+
const skills = [];
128+
for (const id of ids) {
129+
const found = _.find(standSkills, (item) => item.id === id);
130+
if (!found) {
131+
throw new errors.BadRequestError("The skill id is invalid " + id);
132+
}
133+
134+
const skill = {
135+
id,
136+
name: found.name,
137+
};
138+
139+
if (found.category) {
140+
skill.category = {
141+
id: found.category.id,
142+
name: found.category.name,
143+
};
144+
}
145+
146+
skills.push(skill);
147+
}
148+
challenge.skills = skills;
149+
}
150+
102151
async validateCreateChallengeRequest(currentUser, challenge) {
103152
// projectId is required for non self-service challenges
104153
if (challenge.legacy.selfService == null && challenge.projectId == null) {
@@ -125,6 +174,9 @@ class ChallengeHelper {
125174
}
126175
}
127176

177+
// check skills
178+
await this.validateSkills(challenge);
179+
128180
if (challenge.constraints) {
129181
await ChallengeHelper.validateChallengeConstraints(challenge.constraints);
130182
}
@@ -151,6 +203,9 @@ class ChallengeHelper {
151203
}
152204
}
153205

206+
// check skills
207+
await this.validateSkills(data, challenge);
208+
154209
// Ensure descriptionFormat is either 'markdown' or 'html'
155210
if (data.descriptionFormat && !_.includes(["markdown", "html"], data.descriptionFormat)) {
156211
throw new errors.BadRequestError(
@@ -328,6 +383,13 @@ class ChallengeHelper {
328383
delete data.groups;
329384
}
330385

386+
if (data.skills != null) {
387+
data.skillUpdate = {
388+
skills: data.skills,
389+
};
390+
delete data.skills;
391+
}
392+
331393
return data;
332394
}
333395

src/common/helper.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -992,7 +992,11 @@ async function ensureUserCanModifyChallenge(currentUser, challenge, challengeRes
992992
// check groups authorization
993993
await ensureAccessibleByGroupsAccess(currentUser, challenge);
994994
// check full access
995-
const isUserHasFullAccess = await userHasFullAccess(challenge.id, currentUser.userId, challengeResources);
995+
const isUserHasFullAccess = await userHasFullAccess(
996+
challenge.id,
997+
currentUser.userId,
998+
challengeResources
999+
);
9961000
if (
9971001
!currentUser.isMachine &&
9981002
!hasAdminRole(currentUser) &&
@@ -1129,6 +1133,24 @@ async function getMembersByHandles(handles) {
11291133
return res.data;
11301134
}
11311135

1136+
/**
1137+
* Get standard skills by ids
1138+
* @param {Array<String>} ids the skills ids
1139+
* @returns {Object}
1140+
*/
1141+
async function getStandSkills(ids) {
1142+
const token = await m2mHelper.getM2MToken();
1143+
const res = await axios.get(`${config.API_BASE_URL}/v5/standardized-skills/skills`, {
1144+
headers: { Authorization: `Bearer ${token}` },
1145+
params: {
1146+
page: 1,
1147+
perPage: ids.length,
1148+
skillId: ids,
1149+
},
1150+
});
1151+
return res.data;
1152+
}
1153+
11321154
/**
11331155
* Send self service notification
11341156
* @param {String} type the notification type
@@ -1251,6 +1273,7 @@ module.exports = {
12511273
sendSelfServiceNotification,
12521274
getMemberByHandle,
12531275
getMembersByHandles,
1276+
getStandSkills,
12541277
submitZendeskRequest,
12551278
updateSelfServiceProjectInfo,
12561279
getFromInternalCache,

src/common/transformer.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
const _ = require("lodash");
2+
const { compareVersions } = require("compare-versions");
3+
const challengeService = require("../services/ChallengeService");
4+
5+
function transformData(data, fieldsToDelete) {
6+
if (!fieldsToDelete || !fieldsToDelete.length) {
7+
return data;
8+
}
9+
10+
if (_.isArray(data)) {
11+
_.each(data, (item, index) => {
12+
data[index] = transformData(item, fieldsToDelete);
13+
});
14+
} else if (_.isObject(data)) {
15+
for (const field of fieldsToDelete) {
16+
delete data[field];
17+
}
18+
if (data.result) {
19+
data.result = transformData(data.result, fieldsToDelete);
20+
}
21+
}
22+
23+
return data;
24+
}
25+
26+
function transformServices() {
27+
_.each(services, (service, serviceName) => {
28+
const serviceConfig = servicesConfig[serviceName];
29+
if (!serviceConfig) {
30+
return;
31+
}
32+
33+
_.each(service, (method, methodName) => {
34+
service[methodName] = async function () {
35+
const args = Array.prototype.slice.call(arguments);
36+
37+
// No transform need for this method
38+
if (!serviceConfig.methods.includes(methodName)) {
39+
return await method.apply(this, args.slice(1));
40+
}
41+
42+
// args[0] is request, get version header
43+
const request = args[0];
44+
const apiVersion = request.headers["challenge-api-version"] || "1.0.0";
45+
46+
const fieldsToDelete = [];
47+
_.each(serviceConfig.fieldsVersion, (version, field) => {
48+
// If input version less than required version, delete fields
49+
if (compareVersions(apiVersion, version) < 0) {
50+
fieldsToDelete.push(field);
51+
}
52+
});
53+
54+
// Transform request body by deleting fields
55+
if (_.isArray(request.body) || _.isObject(request.body)) {
56+
transformData(request.body, fieldsToDelete);
57+
}
58+
59+
const data = await method.apply(this, args.slice(1));
60+
61+
// Transform response data by deleting fields
62+
return transformData(data, fieldsToDelete);
63+
};
64+
service[methodName].params = ["req", ...method.params];
65+
});
66+
});
67+
}
68+
69+
// Define the version config for services
70+
const servicesConfig = {
71+
challengeService: {
72+
methods: ["searchChallenges", "getChallenge", "createChallenge", "updateChallenge"],
73+
fieldsVersion: {
74+
skills: "1.1.0",
75+
payments: "2.0.0",
76+
},
77+
},
78+
};
79+
80+
// Define the services to export
81+
const services = {
82+
challengeService,
83+
};
84+
85+
// Transform services before export
86+
transformServices();
87+
88+
module.exports = services;

src/controllers/ChallengeController.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Controller for challenge endpoints
33
*/
44
const HttpStatus = require("http-status-codes");
5-
const service = require("../services/ChallengeService");
5+
const { challengeService: service } = require("../common/transformer");
66
const helper = require("../common/helper");
77
const logger = require("../common/logger");
88

@@ -12,7 +12,7 @@ const logger = require("../common/logger");
1212
* @param {Object} res the response
1313
*/
1414
async function searchChallenges(req, res) {
15-
let result = await service.searchChallenges(req.authUser, {
15+
let result = await service.searchChallenges(req, req.authUser, {
1616
...req.query,
1717
...req.body,
1818
});
@@ -23,7 +23,7 @@ async function searchChallenges(req, res) {
2323
logger.debug(`Staring to get mm challengeId`);
2424
const legacyId = await helper.getProjectIdByRoundId(req.query.legacyId);
2525
logger.debug(`Get mm challengeId successfully ${legacyId}`);
26-
result = await service.searchChallenges(req.authUser, {
26+
result = await service.searchChallenges(req, req.authUser, {
2727
...req.query,
2828
...req.body,
2929
legacyId,
@@ -50,7 +50,7 @@ async function createChallenge(req, res) {
5050
logger.debug(
5151
`createChallenge User: ${JSON.stringify(req.authUser)} - Body: ${JSON.stringify(req.body)}`
5252
);
53-
const result = await service.createChallenge(req.authUser, req.body, req.userToken);
53+
const result = await service.createChallenge(req, req.authUser, req.body, req.userToken);
5454
res.status(HttpStatus.CREATED).send(result);
5555
}
5656

@@ -60,7 +60,7 @@ async function createChallenge(req, res) {
6060
* @param {Object} res the response
6161
*/
6262
async function sendNotifications(req, res) {
63-
const result = await service.sendNotifications(req.authUser, req.params.challengeId);
63+
const result = await service.sendNotifications(req, req.authUser, req.params.challengeId);
6464
res.status(HttpStatus.CREATED).send(result);
6565
}
6666

@@ -71,6 +71,7 @@ async function sendNotifications(req, res) {
7171
*/
7272
async function getChallenge(req, res) {
7373
const result = await service.getChallenge(
74+
req,
7475
req.authUser,
7576
req.params.challengeId,
7677
req.query.checkIfExists
@@ -84,7 +85,7 @@ async function getChallenge(req, res) {
8485
* @param {Object} res the response
8586
*/
8687
async function getChallengeStatistics(req, res) {
87-
const result = await service.getChallengeStatistics(req.authUser, req.params.challengeId);
88+
const result = await service.getChallengeStatistics(req, req.authUser, req.params.challengeId);
8889
res.send(result);
8990
}
9091

@@ -99,7 +100,7 @@ async function updateChallenge(req, res) {
99100
req.params.challengeId
100101
} - Body: ${JSON.stringify(req.body)}`
101102
);
102-
const result = await service.updateChallenge(req.authUser, req.params.challengeId, req.body);
103+
const result = await service.updateChallenge(req, req.authUser, req.params.challengeId, req.body);
103104
res.send(result);
104105
}
105106

@@ -112,7 +113,7 @@ async function deleteChallenge(req, res) {
112113
logger.debug(
113114
`deleteChallenge User: ${JSON.stringify(req.authUser)} - ChallengeID: ${req.params.challengeId}`
114115
);
115-
const result = await service.deleteChallenge(req.authUser, req.params.challengeId);
116+
const result = await service.deleteChallenge(req, req.authUser, req.params.challengeId);
116117
res.send(result);
117118
}
118119

@@ -122,7 +123,7 @@ async function deleteChallenge(req, res) {
122123
* @param {Object} res the response
123124
*/
124125
async function advancePhase(req, res) {
125-
res.send(await service.advancePhase(req.authUser, req.params.challengeId, req.body));
126+
res.send(await service.advancePhase(req, req.authUser, req.params.challengeId, req.body));
126127
}
127128

128129
module.exports = {

0 commit comments

Comments
 (0)