diff --git a/.circleci/config.yml b/.circleci/config.yml index e22ef291..7c9ae22c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,7 +11,7 @@ install_dependency: &install_dependency install_deploysuite: &install_deploysuite name: Installation of install_deploysuite. command: | - git clone --branch master https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript + git clone --branch master_hostfix_v1 https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript cp ./../buildscript/master_deploy.sh . cp ./../buildscript/buildenv.sh . cp ./../buildscript/awsconfiguration.sh . diff --git a/config/default.js b/config/default.js index 726c40fa..3298fb10 100644 --- a/config/default.js +++ b/config/default.js @@ -41,6 +41,8 @@ module.exports = { TOPCODER_USERS_API: process.env.TOPCODER_USERS_API || 'https://api.topcoder-dev.com/v3/users', // the api to find topcoder members TOPCODER_MEMBERS_API: process.env.TOPCODER_MEMBERS_API || 'https://api.topcoder-dev.com/v5/members', + // the v3 api to find topcoder members + TOPCODER_MEMBERS_API_V3: process.env.TOPCODER_MEMBERS_API_V3 || 'https://api.topcoder-dev.com/v3/members', // rate limit of requests to user api MAX_PARALLEL_REQUEST_TOPCODER_USERS_API: process.env.MAX_PARALLEL_REQUEST_TOPCODER_USERS_API || 100, diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index a3e40abe..35244bee 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "15f10b58-dda5-4aaf-96e5-061a5c901717", + "_postman_id": "87477d86-2d08-40b6-93c6-99a394193e28", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -19312,6 +19312,247 @@ } ] }, + { + "name": "Member Suggestion", + "item": [ + { + "name": "get member suggestion successfully with administrator", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_administrator}}" + } + ], + "url": { + "raw": "{{URL}}/taas-teams/members-suggest/maxceem", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "members-suggest", + "maxceem" + ] + } + }, + "response": [] + }, + { + "name": "get member suggestion successfully with booking manager", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/taas-teams/members-suggest/isbilir", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "members-suggest", + "isbilir" + ] + } + }, + "response": [] + }, + { + "name": "get member suggestion with connect manager", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_connect_manager_pshahcopmanag2}}" + } + ], + "url": { + "raw": "{{URL}}/taas-teams/members-suggest/isbilir", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "members-suggest", + "isbilir" + ] + } + }, + "response": [] + }, + { + "name": "get member suggestion with member", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_member}}" + } + ], + "url": { + "raw": "{{URL}}/taas-teams/members-suggest/isbilir", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "members-suggest", + "isbilir" + ] + } + }, + "response": [] + }, + { + "name": "get member suggestion with m2m", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"You are not allowed to perform this action!\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_m2m_all_job}}" + } + ], + "url": { + "raw": "{{URL}}/taas-teams/members-suggest/isbilir", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "members-suggest", + "isbilir" + ] + } + }, + "response": [] + }, + { + "name": "get member suggestion with invalid token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 401', function () {\r", + " pm.response.to.have.status(401);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Invalid Token.\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer invalid" + } + ], + "url": { + "raw": "{{URL}}/taas-teams/members-suggest/isbilir", + "host": [ + "{{URL}}" + ], + "path": [ + "taas-teams", + "members-suggest", + "isbilir" + ] + } + }, + "response": [] + } + ] + }, { "name": "GET /taas-teams", "request": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 26b389f4..b7705889 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3296,6 +3296,41 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + /taas-teams/members-suggest/{fragment}: + get: + tags: + - Teams + description: | + Returns suggested members for the given handle fragment + security: + - bearerAuth: [] + responses: + "200": + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/SuggestedMember" + "401": + description: Not authenticated + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "403": + description: Forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /taas-roles: post: tags: @@ -5404,6 +5439,32 @@ components: type: string format: uuid description: "The user Id who updated the role last time.(Will get the user info from the token)" + SuggestedMember: + properties: + userId: + type: number + example: 40157055 + description: the user id + handle: + type: string + example: maxceemdev + description: the user handle + photoURL: + type: string + example: https://topcoder-dev-media.s3.amazonaws.com/member/profile/maxceem13-1587184611143.jpeg + description: the photo url + firstName: + type: string + example: Max + description: the firstname of the user + lastName: + type: string + example: Max + description: the lastname of the user + maxRating: + type: number + example: 1200 + description: the maximum rating of the user RoleRequestBody: required: - name diff --git a/migrations/2021-06-30-role-search-request-make-job-description-longer.js b/migrations/2021-06-30-role-search-request-make-job-description-longer.js new file mode 100644 index 00000000..833bebb4 --- /dev/null +++ b/migrations/2021-06-30-role-search-request-make-job-description-longer.js @@ -0,0 +1,10 @@ +const config = require('config') + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.changeColumn({ tableName: 'role_search_requests', schema: config.DB_SCHEMA_NAME}, 'job_description', {type: Sequelize.STRING(2000)}) + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.changeColumn({ tableName: 'role_search_requests', schema: config.DB_SCHEMA_NAME}, 'job_description', {type: Sequelize.STRING(255)}) + }, +} \ No newline at end of file diff --git a/migrations/2021-07-01-role-insert-custom-role.js b/migrations/2021-07-01-role-insert-custom-role.js new file mode 100644 index 00000000..c6c79101 --- /dev/null +++ b/migrations/2021-07-01-role-insert-custom-role.js @@ -0,0 +1,23 @@ +const config = require('config') +const { v4: uuid } = require('uuid') + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.bulkInsert({ tableName: 'roles', schema: config.DB_SCHEMA_NAME }, [{ + id: uuid(), + name: 'Custom', + rates: [ + { + "global": 1200, + "off_shore": 1200, + "in_country": 1200, + } + ], + created_by: config.m2m.M2M_AUDIT_USER_ID, + created_at: new Date() + }], {}, { rates: { type: Sequelize.ARRAY({ type: Sequelize.JSONB() })}}) + }, + down: async (queryInterface) => { + await queryInterface.bulkDelete({ tableName: 'roles', schema: config.DB_SCHEMA_NAME }, { name: 'Custom' }) + } +} \ No newline at end of file diff --git a/src/common/helper.js b/src/common/helper.js index 5f9f13db..851f6907 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -1981,11 +1981,33 @@ function removeTextFormatting (text) { text = _.replace(text, /[,"'?/\\]/g, ' ') // Replace two or more newlines text = _.replace(text, /\n/g, ' ') + // Replace non-breaking space with regular space + text = _.replace(text, /\xA0/g, ' ') // replace all whitespace characters with single space text = _.replace(text, /\s\s+/g, ' ') return text } +/** + * Function to get member suggestions + * @param {string} fragment the handle fragment + * @return the request result + */ +async function getMembersSuggest (fragment) { + const token = await getM2MToken() + const url = `${config.TOPCODER_MEMBERS_API_V3}/_suggest/${encodeURIComponent(fragment)}` + const res = await request + .get(url) + .set('Authorization', `Bearer ${token}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + localLogger.debug({ + context: 'getMembersSuggest', + message: `response body: ${JSON.stringify(res.body)}` + }) + return res.body +} + module.exports = { getParamFromCliArgs, promptUser, @@ -2046,5 +2068,6 @@ module.exports = { substituteStringByObject, createProject, getMemberGroups, - removeTextFormatting + removeTextFormatting, + getMembersSuggest } diff --git a/src/controllers/TeamController.js b/src/controllers/TeamController.js index 998f9a81..65e5262f 100644 --- a/src/controllers/TeamController.js +++ b/src/controllers/TeamController.js @@ -146,6 +146,15 @@ async function searchSkills (req, res) { res.send(result.result) } +/** + * Suggest members + * @param req the request + * @param res the response + */ +async function suggestMembers (req, res) { + res.send(await service.suggestMembers(req.authUser, req.params.fragment)) +} + module.exports = { searchTeams, getTeam, @@ -159,5 +168,6 @@ module.exports = { getSkillsByJobDescription, roleSearchRequest, createTeam, - searchSkills + searchSkills, + suggestMembers } diff --git a/src/routes/RoleRoutes.js b/src/routes/RoleRoutes.js index ce8441a9..890c9d3d 100644 --- a/src/routes/RoleRoutes.js +++ b/src/routes/RoleRoutes.js @@ -13,13 +13,13 @@ module.exports = { }, get: { controller: 'RoleController', - method: 'searchRoles', + method: 'searchRoles' } }, '/taas-roles/:id': { get: { controller: 'RoleController', - method: 'getRole', + method: 'getRole' }, patch: { controller: 'RoleController', diff --git a/src/routes/TeamRoutes.js b/src/routes/TeamRoutes.js index b2415e17..a4c1ca5e 100644 --- a/src/routes/TeamRoutes.js +++ b/src/routes/TeamRoutes.js @@ -99,5 +99,13 @@ module.exports = { auth: 'jwt', scopes: [constants.Scopes.CREATE_TAAS_TEAM] } + }, + '/taas-teams/members-suggest/:fragment': { + get: { + controller: 'TeamController', + method: 'suggestMembers', + auth: 'jwt', + scopes: [] + } } } diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index 33ba9aa5..a46917aa 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -203,14 +203,14 @@ fullyUpdateJobCandidate.schema = Joi.object() .keys({ jobId: Joi.string().uuid().required(), userId: Joi.string().uuid().required(), - status: Joi.jobCandidateStatus().default("open"), + status: Joi.jobCandidateStatus().default('open'), externalId: Joi.string().allow(null).default(null), - resume: Joi.string().uri().allow("").allow(null).default(null), + resume: Joi.string().uri().allow('').allow(null).default(null), remark: Joi.stringAllowEmpty().allow(null) }) - .required(), + .required() }) - .required(); + .required() /** * Delete jobCandidate by id diff --git a/src/services/JobService.js b/src/services/JobService.js index b4a50665..4a183783 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -294,11 +294,11 @@ partiallyUpdateJob.schema = Joi.object() jobLocation: Joi.stringAllowEmpty().allow(null), jobTimezone: Joi.stringAllowEmpty().allow(null), currency: Joi.stringAllowEmpty().allow(null), - roleIds: Joi.array().items(Joi.string().uuid().required()).allow(null), + roleIds: Joi.array().items(Joi.string().uuid().required()).allow(null) }) - .required(), + .required() }) - .required(); + .required() /** * Fully update job by id diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 94865aae..ea1a5767 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -771,8 +771,11 @@ async function roleSearchRequest (currentUser, data) { // if only job description is provided, collect skill names from description const tags = await getSkillsByJobDescription({ description: data.jobDescription }) const skills = _.map(tags, 'tag') - // find the best matching role - role = await getRoleBySkills(skills) + + // add skills to roleSearchRequest and get best matching role + const [skillIds, roleList] = await Promise.all([getSkillIdsByNames(skills), getRoleBySkills(skills)]) + data.skills = skillIds + role = roleList } data.roleId = role.id // create roleSearchRequest entity with found roleId @@ -789,7 +792,7 @@ roleSearchRequest.schema = Joi.object() currentUser: Joi.object(), data: Joi.object().keys({ roleId: Joi.string().uuid(), - jobDescription: Joi.string().max(255), + jobDescription: Joi.string().max(2000), skills: Joi.array().items(Joi.string().uuid().required()), jobTitle: Joi.string().max(100), previousRoleSearchRequestId: Joi.string().uuid() @@ -864,6 +867,12 @@ async function getSkillsByJobDescription (data) { if (skill.pattern.test(word)) { foundSkills.push(skill.name) } + // for suffix with 'js' + if (!word.endsWith('js') && skill.name.endsWith('js')) { + if (skill.pattern.test(word + 'js')) { + foundSkills.push(skill.name) + } + } }) }) foundSkills = _.uniq(foundSkills) @@ -971,7 +980,7 @@ createRoleSearchRequest.schema = Joi.object() currentUser: Joi.object().required(), roleSearchRequest: Joi.object().keys({ roleId: Joi.string().uuid(), - jobDescription: Joi.string().max(255), + jobDescription: Joi.string().max(2000), skills: Joi.array().items(Joi.string().uuid().required()) }).required().min(1) }).required() @@ -1128,6 +1137,25 @@ searchSkills.schema = Joi.object().keys({ }).required() }).required() +/** + * Get member suggestions + * @param {object} currentUser the user performing the operation. + * @param {string} fragment the user's handle fragment + * @returns {Array} the search result, contains result array + */ +async function suggestMembers (currentUser, fragment) { + if (!currentUser.hasManagePermission) { + throw new errors.ForbiddenError('You are not allowed to perform this action!') + } + const { result } = await helper.getMembersSuggest(fragment) + return result.content +} + +suggestMembers.schema = Joi.object().keys({ + currentUser: Joi.object().required(), + fragment: Joi.string().required() +}).required() + module.exports = { searchTeams, getTeam, @@ -1146,5 +1174,6 @@ module.exports = { createRoleSearchRequest, isExternalMember, createTeam, - searchSkills + searchSkills, + suggestMembers }