From 4f96a8f2184e9ff0ee4863ed7fd8d92da30254d9 Mon Sep 17 00:00:00 2001 From: yoution Date: Thu, 15 Jul 2021 09:29:51 +0800 Subject: [PATCH 01/25] fix: taas-app issue #364 --- data/demo-data.json | 8 +++++++- docs/swagger.yaml | 14 ++++++++++---- src/common/helper.js | 3 +++ src/models/Role.js | 21 +++++++++++++++------ src/services/RoleService.js | 9 ++++++--- src/services/TeamService.js | 2 +- 6 files changed, 42 insertions(+), 15 deletions(-) diff --git a/data/demo-data.json b/data/demo-data.json index aad3fbe7..9d939c4c 100644 --- a/data/demo-data.json +++ b/data/demo-data.json @@ -7732,6 +7732,9 @@ "global": 50, "offShore": 10, "inCountry": 20, + "niche": 50, + "rate20Niche": 20, + "rate30Niche": 10, "rate20Global": 20, "rate30Global": 20, "rate20OffShore": 35, @@ -7743,6 +7746,9 @@ "global": 25, "offShore": 5, "inCountry": 15, + "niche": 50, + "rate20Niche": 20, + "rate30Niche": 10, "rate20Global": 20, "rate30Global": 20, "rate20OffShore": 35, @@ -7762,4 +7768,4 @@ "updatedAt": "2021-05-27T21:43:09.342Z" } ] -} \ No newline at end of file +} diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b7705889..8156b661 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -5555,12 +5555,11 @@ components: example: 300 description: "The time to interview." RoleRates: - required: - - global - - inCountry - - offShore type: object properties: + niche: + type: integer + example: 10 global: type: integer example: 10 @@ -5570,6 +5569,9 @@ components: offShore: type: integer example: 30 + rate30Niche: + type: integer + example: 10 rate30Global: type: integer example: 10 @@ -5579,6 +5581,9 @@ components: rate30OffShore: type: integer example: 30 + rate20Niche: + type: integer + example: 10 rate20Global: type: integer example: 10 @@ -5772,3 +5777,4 @@ components: properties: message: type: string + diff --git a/src/common/helper.js b/src/common/helper.js index 851f6907..0a8ffac5 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -249,6 +249,9 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_ROLE')] = { global: { type: 'integer' }, inCountry: { type: 'integer' }, offShore: { type: 'integer' }, + niche: { type: 'integer' }, + rate20niche: { type: 'integer' }, + rate30niche: { type: 'integer' }, rate30Global: { type: 'integer' }, rate30InCountry: { type: 'integer' }, rate30OffShore: { type: 'integer' }, diff --git a/src/models/Role.js b/src/models/Role.js index ab8b4670..587fec38 100644 --- a/src/models/Role.js +++ b/src/models/Role.js @@ -55,18 +55,27 @@ module.exports = (sequelize) => { type: Sequelize.ARRAY({ type: Sequelize.JSONB({ global: { - type: Sequelize.SMALLINT, - allowNull: false + type: Sequelize.SMALLINT }, inCountry: { field: 'in_country', - type: Sequelize.SMALLINT, - allowNull: false + type: Sequelize.SMALLINT }, offShore: { field: 'off_shore', - type: Sequelize.SMALLINT, - allowNull: false + type: Sequelize.SMALLINT + }, + niche: { + field: 'niche', + type: Sequelize.SMALLINT + }, + rate20Niche: { + field: 'rate20_niche', + type: Sequelize.SMALLINT + }, + rate30Niche: { + field: 'rate30_niche', + type: Sequelize.SMALLINT }, rate30Global: { field: 'rate30_global', diff --git a/src/services/RoleService.js b/src/services/RoleService.js index 765d0f5c..d3b2a908 100644 --- a/src/services/RoleService.js +++ b/src/services/RoleService.js @@ -131,12 +131,15 @@ createRole.schema = Joi.object().keys({ description: Joi.string().max(1000), listOfSkills: Joi.array().items(Joi.string().max(50).required()), rates: Joi.array().items(Joi.object().keys({ - global: Joi.smallint().required(), - inCountry: Joi.smallint().required(), - offShore: Joi.smallint().required(), + global: Joi.smallint(), + inCountry: Joi.smallint(), + offShore: Joi.smallint(), + niche: Joi.smallint(), + rate30Niche: Joi.smallint(), rate30Global: Joi.smallint(), rate30InCountry: Joi.smallint(), rate30OffShore: Joi.smallint(), + rate20Niche: Joi.smallint(), rate20Global: Joi.smallint(), rate20InCountry: Joi.smallint(), rate20OffShore: Joi.smallint() diff --git a/src/services/TeamService.js b/src/services/TeamService.js index ea1a5767..a6ac757e 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -998,7 +998,7 @@ async function _cleanRoleDTO (currentUser, role) { role.isExternalMember = true if (role.rates) { role.rates = _.map(role.rates, rate => - _.omit(rate, ['inCountry', 'offShore', 'rate30InCountry', 'rate30OffShore', 'rate20InCountry', 'rate20OffShore'])) + _.omit(rate, ['inCountry', 'offShore', 'niche', 'rate30InCountry', 'rate30OffShore', 'rate30Niche', 'rate20InCountry', 'rate20OffShore', 'rate20Niche'])) } return role } From a7a9deaa97d4aad89ab1ecb9a8c51fe6ee720a6b Mon Sep 17 00:00:00 2001 From: Michael Baghel Date: Thu, 15 Jul 2021 13:41:14 +0400 Subject: [PATCH 02/25] fix: Make job description length up to 100K characters Resolves: topcoder-platform/taas-app/#374 --- ...-search-request-make-job-description-length-100K.js | 10 ++++++++++ src/models/RoleSearchRequest.js | 2 +- src/services/TeamService.js | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 migrations/2021-07-15-role-search-request-make-job-description-length-100K.js diff --git a/migrations/2021-07-15-role-search-request-make-job-description-length-100K.js b/migrations/2021-07-15-role-search-request-make-job-description-length-100K.js new file mode 100644 index 00000000..08b610dd --- /dev/null +++ b/migrations/2021-07-15-role-search-request-make-job-description-length-100K.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(100000)}) + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.changeColumn({ tableName: 'role_search_requests', schema: config.DB_SCHEMA_NAME}, 'job_description', {type: Sequelize.STRING(2000)}) + }, +} \ No newline at end of file diff --git a/src/models/RoleSearchRequest.js b/src/models/RoleSearchRequest.js index c79ae84f..2e8c189f 100644 --- a/src/models/RoleSearchRequest.js +++ b/src/models/RoleSearchRequest.js @@ -55,7 +55,7 @@ module.exports = (sequelize) => { }, jobDescription: { field: 'job_description', - type: Sequelize.STRING() + type: Sequelize.STRING(100000) }, skills: { type: Sequelize.ARRAY({ diff --git a/src/services/TeamService.js b/src/services/TeamService.js index a6ac757e..617a6dbb 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -792,7 +792,7 @@ roleSearchRequest.schema = Joi.object() currentUser: Joi.object(), data: Joi.object().keys({ roleId: Joi.string().uuid(), - jobDescription: Joi.string().max(2000), + jobDescription: Joi.string().max(100000), skills: Joi.array().items(Joi.string().uuid().required()), jobTitle: Joi.string().max(100), previousRoleSearchRequestId: Joi.string().uuid() From 697d3321334165b1ac4e36ba52f7e2f9c5b64685 Mon Sep 17 00:00:00 2001 From: Michael Baghel Date: Thu, 15 Jul 2021 14:57:26 +0400 Subject: [PATCH 03/25] feat: Add ref code to createTeam and pass to createProject. Resolves: topcoder-platform/taas-app/#365 --- src/services/TeamService.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 617a6dbb..38524362 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -1036,7 +1036,10 @@ async function createTeam (currentUser, data) { description: data.teamDescription, type: 'talent-as-a-service', details: { - positions: data.positions + positions: data.positions, + utm: { + code: data.refCode + } } } // create project with given data @@ -1072,6 +1075,7 @@ createTeam.schema = Joi.object() data: Joi.object().keys({ teamName: Joi.string().required(), teamDescription: Joi.string(), + refCode: Joi.string(), positions: Joi.array().items( Joi.object().keys({ roleName: Joi.string().required(), From f9c0466057ae1deea44c1995c9fbe7e273213666 Mon Sep 17 00:00:00 2001 From: Michael Baghel <31278895+mbaghel@users.noreply.github.com> Date: Thu, 15 Jul 2021 15:55:58 +0400 Subject: [PATCH 04/25] Add refCode to swagger.yaml --- docs/swagger.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 8156b661..62dce550 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -5331,6 +5331,9 @@ components: teamDescription: type: string description: "The description of the team" + refCode: + type: string + description: "Optional referral code" positions: type: array description: "The array of positions" From a25254444e2396fd12e36d54931624ce1948f9e9 Mon Sep 17 00:00:00 2001 From: dengjun Date: Thu, 15 Jul 2021 23:14:41 +0800 Subject: [PATCH 05/25] search candidates API --- src/controllers/JobCandidateController.js | 4 +++- src/services/JobCandidateService.js | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/controllers/JobCandidateController.js b/src/controllers/JobCandidateController.js index 4f81c7ec..a6a31fbd 100644 --- a/src/controllers/JobCandidateController.js +++ b/src/controllers/JobCandidateController.js @@ -2,6 +2,7 @@ * Controller for JobCandidate endpoints */ const HttpStatus = require('http-status-codes') +const _ = require('lodash') const service = require('../services/JobCandidateService') const helper = require('../common/helper') @@ -57,7 +58,8 @@ async function deleteJobCandidate (req, res) { * @param res the response */ async function searchJobCandidates (req, res) { - const result = await service.searchJobCandidates(req.authUser, req.query) + const query = { ...req.query, statuses: _.get(req, 'body.statuses', []) } + const result = await service.searchJobCandidates(req.authUser, query) helper.setResHeaders(req, res, result) res.send(result.result) } diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index a46917aa..b08922e4 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -283,6 +283,15 @@ async function searchJobCandidates (currentUser, criteria) { } }) }) + + // if criteria contains statuses, filter statuses with this value + if (criteria.statuses && criteria.statuses.length > 0) { + esQuery.body.query.bool.filter.push({ + terms: { + status: criteria.statuses + } + }) + } logger.debug({ component: 'JobCandidateService', context: 'searchJobCandidates', message: `Query: ${JSON.stringify(esQuery)}` }) const { body } = await esClient.search(esQuery) @@ -301,10 +310,13 @@ async function searchJobCandidates (currentUser, criteria) { logger.logFullError(err, { component: 'JobCandidateService', context: 'searchJobCandidates' }) } logger.info({ component: 'JobCandidateService', context: 'searchJobCandidates', message: 'fallback to DB query' }) - const filter = {} + const filter = { [Op.and]: [] } _.each(_.pick(criteria, ['jobId', 'userId', 'status', 'externalId']), (value, key) => { filter[Op.and].push({ [key]: value }) }) + if (criteria.statuses && criteria.statuses.length > 0) { + filter[Op.and].push({ status: criteria.statuses }) + } // include interviews if user has permission const include = [] @@ -340,6 +352,7 @@ searchJobCandidates.schema = Joi.object().keys({ jobId: Joi.string().uuid(), userId: Joi.string().uuid(), status: Joi.jobCandidateStatus(), + statuses: Joi.array().items(Joi.jobCandidateStatus()), externalId: Joi.string() }).required() }).required() From 42261e9b93350061f57a392c35c387f1a33d5ddd Mon Sep 17 00:00:00 2001 From: dengjun Date: Fri, 16 Jul 2021 09:41:52 +0800 Subject: [PATCH 06/25] update swagger --- docs/swagger.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 8156b661..7dd9c492 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -624,6 +624,11 @@ paths: schema: type: string description: The external id. + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/JobCandidateSearchBody" responses: "200": description: OK @@ -3996,6 +4001,14 @@ components: type: string example: "topcoder user" description: "The user who updated the job last time.(Will get the user info from the token)" + JobCandidateSearchBody: + properties: + statuses: + type: array + items: + type: string + enum: ["open", "placed", "selected", "client rejected - screening", "client rejected - interview", "rejected - other", "cancelled", "interview", "topcoder-rejected", "applied", "rejected-pre-screen", "skills-test", "phone-screen", "job-closed", "offered"] + description: "The array of job Candidates status" JobCandidateRequestBody: required: - jobId From d60303829eec7f5433ad0d5792f586c24cecceba Mon Sep 17 00:00:00 2001 From: dengjun Date: Sat, 17 Jul 2021 13:55:54 +0800 Subject: [PATCH 07/25] rb jobIds added --- docs/swagger.yaml | 13 +++++++++++++ src/controllers/ResourceBookingController.js | 4 +++- src/services/ResourceBookingService.js | 19 ++++++++++++++++--- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e566edf7..48d1efb0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1557,6 +1557,11 @@ paths: maximum: 5 example: 3 description: The workdays to pay + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ResourceBookingSearchBody" responses: "200": @@ -4449,6 +4454,14 @@ components: type: string example: "topcoder user" description: "The user who updated the job last time.(Will get the user info from the token)" + ResourceBookingSearchBody: + properties: + jobIds: + type: array + items: + type: string + format: uuid + description: "The array of job ids" ResourceBookingRequestBody: required: - projectId diff --git a/src/controllers/ResourceBookingController.js b/src/controllers/ResourceBookingController.js index f8d3d566..24fafcd7 100644 --- a/src/controllers/ResourceBookingController.js +++ b/src/controllers/ResourceBookingController.js @@ -2,6 +2,7 @@ * Controller for ResourceBooking endpoints */ const HttpStatus = require('http-status-codes') +const _ = require('lodash') const service = require('../services/ResourceBookingService') const helper = require('../common/helper') @@ -57,7 +58,8 @@ async function deleteResourceBooking (req, res) { * @param res the response */ async function searchResourceBookings (req, res) { - const result = await service.searchResourceBookings(req.authUser, req.query) + const query = { ...req.query, jobIds: _.get(req, 'body.jobIds', []) } + const result = await service.searchResourceBookings(req.authUser, query) helper.setResHeaders(req, res, result) res.send(result.result) } diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index cabadca0..e3411588 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -556,7 +556,8 @@ async function searchResourceBookings (currentUser, criteria, options) { body: { query: { bool: { - must: [] + must: [], + filter: [] } }, from: (page - 1) * perPage, @@ -600,11 +601,19 @@ async function searchResourceBookings (currentUser, criteria, options) { }) // if criteria contains projectIds, filter projectId with this value if (criteria.projectIds) { - esQuery.body.query.bool.filter = [{ + esQuery.body.query.bool.filter.push({ terms: { projectId: criteria.projectIds } - }] + }) + } + // if criteria contains jobIds, filter jobIds with this value + if (criteria.jobIds && criteria.jobIds.length > 0) { + esQuery.body.query.bool.filter.push({ + terms: { + jobId: criteria.jobIds + } + }) } // Apply WorkPeriod and WorkPeriodPayment filters const workPeriodFilters = _.pick(criteria, ['workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle']) @@ -710,6 +719,9 @@ async function searchResourceBookings (currentUser, criteria, options) { if (criteria.projectIds) { filter[Op.and].push({ projectId: criteria.projectIds }) } + if (criteria.jobIds && criteria.jobIds.length > 0) { + filter[Op.and].push({ id: criteria.jobIds }) + } const queryCriteria = { where: filter, offset: ((page - 1) * perPage), @@ -831,6 +843,7 @@ searchResourceBookings.schema = Joi.object().keys({ endDate: Joi.date().format('YYYY-MM-DD'), rateType: Joi.rateType(), jobId: Joi.string().uuid(), + jobIds: Joi.array().items(Joi.string().uuid()), userId: Joi.string().uuid(), projectId: Joi.number().integer(), projectIds: Joi.alternatives( From 7a012ce7cde506451198a8370271d997be802737 Mon Sep 17 00:00:00 2001 From: yoution Date: Mon, 19 Jul 2021 14:42:02 +0800 Subject: [PATCH 08/25] fix: issue-370 add nichrate for update api --- src/services/RoleService.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/RoleService.js b/src/services/RoleService.js index d3b2a908..7ff7de65 100644 --- a/src/services/RoleService.js +++ b/src/services/RoleService.js @@ -192,10 +192,13 @@ updateRole.schema = Joi.object().keys({ global: Joi.smallint().required(), inCountry: Joi.smallint().required(), offShore: Joi.smallint().required(), + niche: Joi.smallint(), + rate30Niche: Joi.smallint(), rate30Global: Joi.smallint(), rate30InCountry: Joi.smallint(), rate30OffShore: Joi.smallint(), rate20Global: Joi.smallint(), + rate20Niche: Joi.smallint(), rate20InCountry: Joi.smallint(), rate20OffShore: Joi.smallint() }).required()), From c8d5c50a94cebb52d5e536b97eb014b021ede5ae Mon Sep 17 00:00:00 2001 From: dengjun Date: Mon, 19 Jul 2021 18:46:33 +0800 Subject: [PATCH 09/25] fix:fields param in RB --- src/services/ResourceBookingService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index e3411588..45f5cac1 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -109,7 +109,7 @@ function _checkCriteriaAndGetFields (currentUser, criteria) { // Check if any WorkPeriodPayment field will be returned result.withWorkPeriodPayments = result.allWorkPeriodPayments || result.fieldsWPP.length > 0 // Extract the filters from criteria parameter - let filters = _.filter(Object.keys(criteria), key => _.indexOf(['fromDb', 'fields', 'page', 'perPage', 'sortBy', 'sortOrder'], key) === -1) + let filters = _.filter(Object.keys(criteria), key => _.indexOf(['fromDb', 'fields', 'page', 'perPage', 'sortBy', 'sortOrder', 'jobIds'], key) === -1) filters = _.map(filters, f => { if (f === 'projectIds') { return 'projectId' From 0c858ca4760540bd69c912417493ddeeeb9d7d87 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Tue, 20 Jul 2021 00:30:23 +0300 Subject: [PATCH 10/25] new filter for RB --- ...coder-bookings-api.postman_collection.json | 242 +++++++++++++++++- docs/swagger.yaml | 30 ++- src/common/helper.js | 2 +- src/services/ResourceBookingService.js | 60 ++++- test/unit/common/ResourceBookingData.js | 2 +- 5 files changed, 327 insertions(+), 9 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 35244bee..2a526d21 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "87477d86-2d08-40b6-93c6-99a394193e28", + "_postman_id": "0bd597ba-4bc2-4ea1-be33-45776b80c1ce", "name": "Topcoder-bookings-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -11647,6 +11647,11 @@ "key": "projectIds", "value": "111, 16705", "disabled": true + }, + { + "key": "billingAccountId", + "value": "0", + "disabled": true } ] } @@ -11725,6 +11730,11 @@ "key": "status", "value": "assigned", "disabled": true + }, + { + "key": "billingAccountId", + "value": "0", + "disabled": true } ] } @@ -11797,6 +11807,11 @@ { "key": "sortOrder", "value": "desc" + }, + { + "key": "billingAccountId", + "value": "0", + "disabled": true } ] } @@ -11867,6 +11882,11 @@ { "key": "sortOrder", "value": "desc" + }, + { + "key": "billingAccountId", + "value": "0", + "disabled": true } ] } @@ -12537,6 +12557,226 @@ }, "response": [] }, + { + "name": "search resource bookings with parameters 13", + "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}}/resourceBookings?fields=id,startDate,endDate,billingAccountId,workPeriods&billingAccountId=80000071&workPeriods.startDate=2021-01-03&workPeriods.isFirstWeek=true", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "id,startDate,endDate,billingAccountId,workPeriods" + }, + { + "key": "billingAccountId", + "value": "80000071" + }, + { + "key": "workPeriods.startDate", + "value": "2021-01-03" + }, + { + "key": "workPeriods.isFirstWeek", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 14", + "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}}/resourceBookings?fields=id,startDate,endDate,billingAccountId,workPeriods&billingAccountId=80000071&workPeriods.startDate=2021-02-07&workPeriods.isLastWeek=true", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "id,startDate,endDate,billingAccountId,workPeriods" + }, + { + "key": "billingAccountId", + "value": "80000071" + }, + { + "key": "workPeriods.startDate", + "value": "2021-02-07" + }, + { + "key": "workPeriods.isLastWeek", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 15", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Cannot set \\\"isFirstWeek\\\" without \\\"startDate\\\"\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?fields=id,startDate,endDate,billingAccountId,workPeriods&billingAccountId=80000071&workPeriods.isLastWeek=true", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "id,startDate,endDate,billingAccountId,workPeriods" + }, + { + "key": "billingAccountId", + "value": "80000071" + }, + { + "key": "workPeriods.isLastWeek", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 16", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Cannot set both \\\"isFirstWeek\\\" and \\\"isLastWeek\\\" to \\\"true\\\"\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?fields=id,startDate,endDate,billingAccountId,workPeriods&billingAccountId=80000071&workPeriods.startDate=2021-02-07&workPeriods.isLastWeek=true&workPeriods.isFirstWeek=true", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "id,startDate,endDate,billingAccountId,workPeriods" + }, + { + "key": "billingAccountId", + "value": "80000071" + }, + { + "key": "workPeriods.startDate", + "value": "2021-02-07" + }, + { + "key": "workPeriods.isLastWeek", + "value": "true" + }, + { + "key": "workPeriods.isFirstWeek", + "value": "true" + } + ] + } + }, + "response": [] + }, { "name": "put resource booking with booking manager", "event": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 48d1efb0..6bec35a5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1506,6 +1506,12 @@ paths: schema: type: string description: comma separated project ids. + - in: query + name: billingAccountId + required: false + schema: + type: integer + description: billing account id. 0 represents null value. - in: query name: workPeriods.paymentStatus required: false @@ -1527,7 +1533,7 @@ paths: type: string format: date pattern: '^\d{4}-\d{2}-\d{2}$' - description: The work period start date. + description: The work period start date. Should be Sunday. - in: query name: workPeriods.endDate required: false @@ -1535,13 +1541,33 @@ paths: type: string format: date pattern: '^\d{4}-\d{2}-\d{2}$' - description: The work period end date. + description: The work period end date. Should be Saturday. - in: query name: workPeriods.userHandle required: false schema: type: string description: The user handle. + - in: query + name: workPeriods.isFirstWeek + required: false + schema: + type: boolean + default: false + description: | + the week which matches workPeriods.startDate is the first one in the RB. + workPeriods.startDate is required. + only one of workPeriods.isFirstWeek and workPeriods.isLastWeek is allowed. + - in: query + name: workPeriods.isLastWeek + required: false + schema: + type: boolean + default: false + description: | + the week which matches workPeriods.startDate is the last one in the RB. + workPeriods.startDate is required. + only one of workPeriods.isFirstWeek and workPeriods.isLastWeek is allowed. - in: query name: workPeriods.payments.status required: false diff --git a/src/common/helper.js b/src/common/helper.js index 0a8ffac5..7f9625be 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -177,7 +177,7 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = { memberRate: { type: 'float' }, customerRate: { type: 'float' }, rateType: { type: 'keyword' }, - billingAccountId: { type: 'integer' }, + billingAccountId: { type: 'integer', null_value: 0 }, workPeriods: { type: 'nested', properties: { diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 45f5cac1..afebc01a 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -109,7 +109,7 @@ function _checkCriteriaAndGetFields (currentUser, criteria) { // Check if any WorkPeriodPayment field will be returned result.withWorkPeriodPayments = result.allWorkPeriodPayments || result.fieldsWPP.length > 0 // Extract the filters from criteria parameter - let filters = _.filter(Object.keys(criteria), key => _.indexOf(['fromDb', 'fields', 'page', 'perPage', 'sortBy', 'sortOrder', 'jobIds'], key) === -1) + let filters = _.filter(Object.keys(criteria), key => _.indexOf(['fromDb', 'fields', 'page', 'perPage', 'sortBy', 'sortOrder', 'jobIds', 'workPeriods.isFirstWeek', 'workPeriods.isLastWeek'], key) === -1) filters = _.map(filters, f => { if (f === 'projectIds') { return 'projectId' @@ -590,7 +590,7 @@ async function searchResourceBookings (currentUser, criteria, options) { } esQuery.body.sort.push(sort) // Apply ResourceBooking filters - _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId']), (value, key) => { + _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId', 'billingAccountId']), (value, key) => { esQuery.body.query.bool.must.push({ term: { [key]: { @@ -615,6 +615,15 @@ async function searchResourceBookings (currentUser, criteria, options) { } }) } + if (criteria['workPeriods.isFirstWeek']) { + esQuery.body.query.bool.must.push({ + range: { startDate: { gte: criteria['workPeriods.startDate'] } } + }) + } else if (criteria['workPeriods.isLastWeek']) { + esQuery.body.query.bool.must.push({ + range: { endDate: { lte: moment(criteria['workPeriods.startDate']).add(6, 'day').format('YYYY-MM-DD') } } + }) + } // Apply WorkPeriod and WorkPeriodPayment filters const workPeriodFilters = _.pick(criteria, ['workPeriods.paymentStatus', 'workPeriods.startDate', 'workPeriods.endDate', 'workPeriods.userHandle']) const workPeriodPaymentFilters = _.pick(criteria, ['workPeriods.payments.status', 'workPeriods.payments.days']) @@ -716,12 +725,20 @@ async function searchResourceBookings (currentUser, criteria, options) { _.each(_.pick(criteria, ['status', 'startDate', 'endDate', 'rateType', 'projectId', 'jobId', 'userId']), (value, key) => { filter[Op.and].push({ [key]: value }) }) + if (!_.isUndefined(criteria.billingAccountId)) { + filter[Op.and].push({ billingAccountId: criteria.billingAccountId === 0 ? null : criteria.billingAccountId }) + } if (criteria.projectIds) { filter[Op.and].push({ projectId: criteria.projectIds }) } if (criteria.jobIds && criteria.jobIds.length > 0) { filter[Op.and].push({ id: criteria.jobIds }) } + if (criteria['workPeriods.isFirstWeek']) { + filter[Op.and].push({ startDate: { [Op.gte]: criteria['workPeriods.startDate'] } }) + } else if (criteria['workPeriods.isLastWeek']) { + filter[Op.and].push({ endDate: { [Op.lte]: moment(criteria['workPeriods.startDate']).add(6, 'day').format('YYYY-MM-DD') } }) + } const queryCriteria = { where: filter, offset: ((page - 1) * perPage), @@ -850,13 +867,48 @@ searchResourceBookings.schema = Joi.object().keys({ Joi.string(), Joi.array().items(Joi.number().integer()) ), + billingAccountId: Joi.number().integer(), 'workPeriods.paymentStatus': Joi.alternatives( Joi.string(), Joi.array().items(Joi.paymentStatus()) ), - 'workPeriods.startDate': Joi.date().format('YYYY-MM-DD'), - 'workPeriods.endDate': Joi.date().format('YYYY-MM-DD'), + 'workPeriods.startDate': Joi.date().format('YYYY-MM-DD').custom((value, helpers) => { + const date = new Date(value) + const weekDay = date.getDay() + if (weekDay !== 0) { + return helpers.message('workPeriods.startDate should be always Sunday') + } + return value + }), + 'workPeriods.endDate': Joi.date().format('YYYY-MM-DD').custom((value, helpers) => { + const date = new Date(value) + const weekDay = date.getDay() + if (weekDay !== 6) { + return helpers.message('workPeriods.endDate should be always Saturday') + } + return value + }), 'workPeriods.userHandle': Joi.string(), + 'workPeriods.isFirstWeek': Joi.when(Joi.ref('workPeriods.startDate', { separator: false }), { + is: Joi.exist(), + then: Joi.boolean().default(false), + otherwise: Joi.boolean().valid(false).messages({ + 'any.only': 'Cannot set "isFirstWeek" without "startDate"' + }) + }), + 'workPeriods.isLastWeek': Joi.boolean().when(Joi.ref('workPeriods.startDate', { separator: false }), { + is: Joi.exist(), + then: Joi.when(Joi.ref('workPeriods.isFirstWeek', { separator: false }), { + is: false, + then: Joi.boolean().default(false), + otherwise: Joi.boolean().valid(false).messages({ + 'any.only': 'Cannot set both "isFirstWeek" and "isLastWeek" to "true"' + }) + }), + otherwise: Joi.boolean().valid(false).messages({ + 'any.only': 'Cannot set "isFirstWeek" without "startDate"' + }) + }), 'workPeriods.payments.status': Joi.workPeriodPaymentStatus(), 'workPeriods.payments.days': Joi.number().integer().min(0).max(5) }).required(), diff --git a/test/unit/common/ResourceBookingData.js b/test/unit/common/ResourceBookingData.js index 1f9535a1..0dd6aa74 100644 --- a/test/unit/common/ResourceBookingData.js +++ b/test/unit/common/ResourceBookingData.js @@ -1500,7 +1500,7 @@ const T30 = { } } const T31 = { - criteria: { 'workPeriods.startDate': '2021-05-10' }, + criteria: { 'workPeriods.startDate': '2021-05-09' }, error: { httpStatus: 400, message: 'Can not filter or sort by some field which is not included in fields' From b5942becc26511972cb53ca8da7512c478403d20 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Tue, 20 Jul 2021 14:19:02 +0800 Subject: [PATCH 11/25] add withdrawn status --- src/bootstrap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bootstrap.js b/src/bootstrap.js index 6e088046..dad6c133 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -15,7 +15,7 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly', Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') -Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed', 'offered') +Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed', 'offered', 'withdrawn') Joi.title = () => Joi.string().max(128) Joi.paymentStatus = () => Joi.string().valid(..._.values(AggregatePaymentStatus)) Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) From f7e81ebc15c876547027ff23714ec0bee07666f4 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Tue, 20 Jul 2021 18:41:17 +0300 Subject: [PATCH 12/25] rephrase validation messages --- ...coder-bookings-api.postman_collection.json | 56 ++++++++++++++++++- src/services/ResourceBookingService.js | 6 +- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 2a526d21..04ab034f 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -12675,7 +12675,7 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"Cannot set \\\"isFirstWeek\\\" without \\\"startDate\\\"\")\r", + " pm.expect(response.message).to.eq(\"Cannot filter by \\\"isLastWeek\\\" without \\\"startDate\\\"\")\r", "});" ], "type": "text/javascript" @@ -12727,7 +12727,59 @@ "pm.test('Status code is 400', function () {\r", " pm.response.to.have.status(400);\r", " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"Cannot set both \\\"isFirstWeek\\\" and \\\"isLastWeek\\\" to \\\"true\\\"\")\r", + " pm.expect(response.message).to.eq(\"Cannot filter by \\\"isFirstWeek\\\" without \\\"startDate\\\"\")\r", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "type": "text", + "value": "Bearer {{token_bookingManager}}" + } + ], + "url": { + "raw": "{{URL}}/resourceBookings?fields=id,startDate,endDate,billingAccountId,workPeriods&billingAccountId=80000071&workPeriods.isFirstWeek=true", + "host": [ + "{{URL}}" + ], + "path": [ + "resourceBookings" + ], + "query": [ + { + "key": "fields", + "value": "id,startDate,endDate,billingAccountId,workPeriods" + }, + { + "key": "billingAccountId", + "value": "80000071" + }, + { + "key": "workPeriods.isFirstWeek", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "search resource bookings with parameters 17", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + " const response = pm.response.json()\r", + " pm.expect(response.message).to.eq(\"Cannot filter by both \\\"isFirstWeek\\\" and \\\"isLastWeek\\\" set to \\\"true\\\"\")\r", "});" ], "type": "text/javascript" diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index afebc01a..7c5d0a59 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -893,7 +893,7 @@ searchResourceBookings.schema = Joi.object().keys({ is: Joi.exist(), then: Joi.boolean().default(false), otherwise: Joi.boolean().valid(false).messages({ - 'any.only': 'Cannot set "isFirstWeek" without "startDate"' + 'any.only': 'Cannot filter by "isFirstWeek" without "startDate"' }) }), 'workPeriods.isLastWeek': Joi.boolean().when(Joi.ref('workPeriods.startDate', { separator: false }), { @@ -902,11 +902,11 @@ searchResourceBookings.schema = Joi.object().keys({ is: false, then: Joi.boolean().default(false), otherwise: Joi.boolean().valid(false).messages({ - 'any.only': 'Cannot set both "isFirstWeek" and "isLastWeek" to "true"' + 'any.only': 'Cannot filter by both "isFirstWeek" and "isLastWeek" set to "true"' }) }), otherwise: Joi.boolean().valid(false).messages({ - 'any.only': 'Cannot set "isFirstWeek" without "startDate"' + 'any.only': 'Cannot filter by "isLastWeek" without "startDate"' }) }), 'workPeriods.payments.status': Joi.workPeriodPaymentStatus(), From 05e64dce026399856f16637edf909dc3b3800429 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 21 Jul 2021 00:51:23 +0300 Subject: [PATCH 13/25] deny process payment for future WP --- src/services/WorkPeriodPaymentService.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/WorkPeriodPaymentService.js b/src/services/WorkPeriodPaymentService.js index 72b94d44..9a7a7780 100644 --- a/src/services/WorkPeriodPaymentService.js +++ b/src/services/WorkPeriodPaymentService.js @@ -75,6 +75,10 @@ async function _createSingleWorkPeriodPaymentWithWorkPeriodAndResourceBooking (w if (maxPossibleDays <= 0) { throw new errors.ConflictError(`There are no days to pay for WorkPeriod: ${correspondingWorkPeriod.id}`) } + const workPeriodStartTime = moment(`${correspondingWorkPeriod.startDate}T00:00:00.000+12`) + if (workPeriodStartTime.isAfter(moment())) { + throw new errors.BadRequestError(`Cannot process payments for the future WorkPeriods. You can process after ${workPeriodStartTime.diff(moment(), 'hours')} hours`) + } workPeriodPayment.days = _.defaultTo(workPeriodPayment.days, maxPossibleDays) workPeriodPayment.amount = _.round(workPeriodPayment.memberRate * workPeriodPayment.days / 5, 2) workPeriodPayment.customerRate = _.defaultTo(correspondingResourceBooking.customerRate, null) From 4c8d2687dcd290c6b925b5e5f5d93a617d82e6ff Mon Sep 17 00:00:00 2001 From: yoution Date: Wed, 21 Jul 2021 10:13:32 +0800 Subject: [PATCH 14/25] fix: taas issue #402 --- src/services/TeamService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 38524362..375f5c2f 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -980,7 +980,7 @@ createRoleSearchRequest.schema = Joi.object() currentUser: Joi.object().required(), roleSearchRequest: Joi.object().keys({ roleId: Joi.string().uuid(), - jobDescription: Joi.string().max(2000), + jobDescription: Joi.string().max(100000), skills: Joi.array().items(Joi.string().uuid().required()) }).required().min(1) }).required() From 46b42034b4ae861d3fbd54c09710cc646ff09707 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Wed, 21 Jul 2021 13:37:08 +0800 Subject: [PATCH 15/25] add two new jc status --- src/bootstrap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bootstrap.js b/src/bootstrap.js index dad6c133..a81e5dcc 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -15,7 +15,7 @@ Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly', Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') Joi.resourceBookingStatus = () => Joi.string().valid('placed', 'closed', 'cancelled') Joi.workload = () => Joi.string().valid('full-time', 'fractional') -Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed', 'offered', 'withdrawn') +Joi.jobCandidateStatus = () => Joi.string().valid('open', 'placed', 'selected', 'client rejected - screening', 'client rejected - interview', 'rejected - other', 'cancelled', 'interview', 'topcoder-rejected', 'applied', 'rejected-pre-screen', 'skills-test', 'skills-test', 'phone-screen', 'job-closed', 'offered', 'withdrawn', 'withdrawn-prescreen') Joi.title = () => Joi.string().max(128) Joi.paymentStatus = () => Joi.string().valid(..._.values(AggregatePaymentStatus)) Joi.xaiTemplate = () => Joi.string().valid(...allowedXAITemplate) From d5c3105fd738eb8a6e63091cc661675d39dde89c Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Wed, 21 Jul 2021 13:43:18 +0800 Subject: [PATCH 16/25] update swagger --- docs/swagger.yaml | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6bec35a5..6eb3ef5d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3937,6 +3937,14 @@ components: "cancelled", "interview", "topcoder-rejected", + "applied", + "rejected-pre-screen", + "skills-test", + "phone-screen", + "job-closed", + "offered", + "withdrawn", + "withdrawn-prescreen" ] description: "The job candidate status." externalId: @@ -4038,7 +4046,7 @@ components: type: array items: type: string - enum: ["open", "placed", "selected", "client rejected - screening", "client rejected - interview", "rejected - other", "cancelled", "interview", "topcoder-rejected", "applied", "rejected-pre-screen", "skills-test", "phone-screen", "job-closed", "offered"] + enum: ["open", "placed", "selected", "client rejected - screening", "client rejected - interview", "rejected - other", "cancelled", "interview", "topcoder-rejected", "applied", "rejected-pre-screen", "skills-test", "phone-screen", "job-closed", "offered", "withdrawn", "withdrawn-prescreen"] description: "The array of job Candidates status" JobCandidateRequestBody: required: @@ -4067,6 +4075,14 @@ components: "cancelled", "interview", "topcoder-rejected", + "applied", + "rejected-pre-screen", + "skills-test", + "phone-screen", + "job-closed", + "offered", + "withdrawn", + "withdrawn-prescreen" ] description: "The job candidate status." default: open @@ -4097,6 +4113,14 @@ components: "cancelled", "interview", "topcoder-rejected", + "applied", + "rejected-pre-screen", + "skills-test", + "phone-screen", + "job-closed", + "offered", + "withdrawn", + "withdrawn-prescreen" ] externalId: type: string @@ -5241,6 +5265,14 @@ components: "cancelled", "interview", "topcoder-rejected", + "applied", + "rejected-pre-screen", + "skills-test", + "phone-screen", + "job-closed", + "offered", + "withdrawn", + "withdrawn-prescreen" ] description: "The job candidate status." skills: From 740bde74d79c6391d47f5cd0cbf289a85b5d653b Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 21 Jul 2021 12:26:21 +0300 Subject: [PATCH 17/25] allow filter for both isFirstWeek and isLastWeek --- ...coder-bookings-api.postman_collection.json | 60 ------------------- src/services/ResourceBookingService.js | 14 ++--- 2 files changed, 5 insertions(+), 69 deletions(-) diff --git a/docs/Topcoder-bookings-api.postman_collection.json b/docs/Topcoder-bookings-api.postman_collection.json index 04ab034f..8ee3ef14 100644 --- a/docs/Topcoder-bookings-api.postman_collection.json +++ b/docs/Topcoder-bookings-api.postman_collection.json @@ -12769,66 +12769,6 @@ }, "response": [] }, - { - "name": "search resource bookings with parameters 17", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code is 400', function () {\r", - " pm.response.to.have.status(400);\r", - " const response = pm.response.json()\r", - " pm.expect(response.message).to.eq(\"Cannot filter by both \\\"isFirstWeek\\\" and \\\"isLastWeek\\\" set to \\\"true\\\"\")\r", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "type": "text", - "value": "Bearer {{token_bookingManager}}" - } - ], - "url": { - "raw": "{{URL}}/resourceBookings?fields=id,startDate,endDate,billingAccountId,workPeriods&billingAccountId=80000071&workPeriods.startDate=2021-02-07&workPeriods.isLastWeek=true&workPeriods.isFirstWeek=true", - "host": [ - "{{URL}}" - ], - "path": [ - "resourceBookings" - ], - "query": [ - { - "key": "fields", - "value": "id,startDate,endDate,billingAccountId,workPeriods" - }, - { - "key": "billingAccountId", - "value": "80000071" - }, - { - "key": "workPeriods.startDate", - "value": "2021-02-07" - }, - { - "key": "workPeriods.isLastWeek", - "value": "true" - }, - { - "key": "workPeriods.isFirstWeek", - "value": "true" - } - ] - } - }, - "response": [] - }, { "name": "put resource booking with booking manager", "event": [ diff --git a/src/services/ResourceBookingService.js b/src/services/ResourceBookingService.js index 7c5d0a59..46d2fe62 100644 --- a/src/services/ResourceBookingService.js +++ b/src/services/ResourceBookingService.js @@ -619,7 +619,8 @@ async function searchResourceBookings (currentUser, criteria, options) { esQuery.body.query.bool.must.push({ range: { startDate: { gte: criteria['workPeriods.startDate'] } } }) - } else if (criteria['workPeriods.isLastWeek']) { + } + if (criteria['workPeriods.isLastWeek']) { esQuery.body.query.bool.must.push({ range: { endDate: { lte: moment(criteria['workPeriods.startDate']).add(6, 'day').format('YYYY-MM-DD') } } }) @@ -736,7 +737,8 @@ async function searchResourceBookings (currentUser, criteria, options) { } if (criteria['workPeriods.isFirstWeek']) { filter[Op.and].push({ startDate: { [Op.gte]: criteria['workPeriods.startDate'] } }) - } else if (criteria['workPeriods.isLastWeek']) { + } + if (criteria['workPeriods.isLastWeek']) { filter[Op.and].push({ endDate: { [Op.lte]: moment(criteria['workPeriods.startDate']).add(6, 'day').format('YYYY-MM-DD') } }) } const queryCriteria = { @@ -898,13 +900,7 @@ searchResourceBookings.schema = Joi.object().keys({ }), 'workPeriods.isLastWeek': Joi.boolean().when(Joi.ref('workPeriods.startDate', { separator: false }), { is: Joi.exist(), - then: Joi.when(Joi.ref('workPeriods.isFirstWeek', { separator: false }), { - is: false, - then: Joi.boolean().default(false), - otherwise: Joi.boolean().valid(false).messages({ - 'any.only': 'Cannot filter by both "isFirstWeek" and "isLastWeek" set to "true"' - }) - }), + then: Joi.boolean().default(false), otherwise: Joi.boolean().valid(false).messages({ 'any.only': 'Cannot filter by "isLastWeek" without "startDate"' }) From 791ae335c4f0632f019df89d12be5ee1f9336db8 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Wed, 21 Jul 2021 20:12:16 +0800 Subject: [PATCH 18/25] add automation script feature --- config/default.js | 12 +++ src/eventHandlers/JobCandidateEventHandler.js | 84 ++++++++++++++++++- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/config/default.js b/config/default.js index 3298fb10..862f71e9 100644 --- a/config/default.js +++ b/config/default.js @@ -214,5 +214,17 @@ module.exports = { FIX_DELAY_STEP_ASSIGN_MEMBER: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_ASSIGN_MEMBER || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500), // the fix delay after step of activate challenge, unit: ms FIX_DELAY_STEP_ACTIVATE_CHALLENGE: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_ACTIVATE_CHALLENGE || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500) + }, + // if a job reach this critier, system will automatically withdrawn other job applications. + JOBS_HOUR_PER_WEEK: 20, + // status mapping + STATUS_MAPPING: { + applied: 'withdrawn-prescreen', + 'skills-test': 'withdrawn-prescreen', + 'phone-screen': 'withdrawn-prescreen', + open: 'withdrawn', + interview: 'withdrawn', + selected: 'withdrawn', + offered: 'withdrawn' } } diff --git a/src/eventHandlers/JobCandidateEventHandler.js b/src/eventHandlers/JobCandidateEventHandler.js index 8780ab08..a964d459 100644 --- a/src/eventHandlers/JobCandidateEventHandler.js +++ b/src/eventHandlers/JobCandidateEventHandler.js @@ -1,11 +1,14 @@ /* * Handle events for JobCandidate. */ - +const { Op } = require('sequelize') +const _ = require('lodash') +const config = require('config') const models = require('../models') const logger = require('../common/logger') const helper = require('../common/helper') const JobService = require('../services/JobService') +const JobCandidateService = require('../services/JobCandidateService') /** * Once we create at least one JobCandidate for a Job, the Job status should be changed to in-review. @@ -44,6 +47,84 @@ async function inReviewJob (payload) { } } +/** + * Actual Update Job Candidates + * + * @param {*} statuses the source status we'll update + * @param {*} userId the userID + */ +async function updateJobCandidates (statuses, userId) { + logger.info({ + component: 'JobCandidateEventHandler', + context: 'updateJobCandidates', + message: `Update jobCandidates for user ${userId}` + }) + const filter = { [Op.and]: [] } + filter[Op.and].push({ status: statuses }) + filter[Op.and].push({ userId: userId }) + const candidates = await models.JobCandidate.findAll({ + where: filter + }) + if (candidates && candidates.length > 0) { + _.each(candidates, async (candidate) => { + logger.info({ + component: 'JobCandidateEventHandler', + context: 'updateJobCandidates', + message: `Begining update id: ${candidate.id}' candidate with ${candidate.status} status into ${config.STATUS_MAPPING[candidate.status]} for userId: ${userId}` + }) + await JobCandidateService.partiallyUpdateJobCandidate( + helper.getAuditM2Muser(), + candidate.id, + { status: config.STATUS_MAPPING[candidate.status] } + ).then(result => { + logger.info({ + component: 'JobCandidateEventHandler', + context: 'updateJobCandidates', + message: `Finishing update id: ${result.id}' candidate into ${result.status} status for userId: ${userId}` + }) + }) + }) + } else { + logger.info({ + component: 'JobCandidateEventHandler', + context: 'updateJobCandidates', + message: `There are not jobCandidates for user ${userId} that required to be updated.` + }) + } +} + +/** + * Update Job Candidates based on business rules + * + * @param {*} payload the updated jobCandidate info + */ +async function withDrawnJobCandidates (payload) { + const jobCandidate = payload.value + if (jobCandidate.status === 'placed') { + const job = await models.Job.findById(payload.value.jobId) + if (job.hoursPerWeek > config.JOBS_HOUR_PER_WEEK) { + // find all these user's open job Candidate and mark the status as withdrawn or withdrawn-prescreen + logger.info({ + component: 'JobCandidateEventHandler', + context: 'withDrawnJobCandidates', + message: `Begining update jobCandidates as ${payload.value.id} candidate's new gig is requiring 20 hrs per week` + }) + await updateJobCandidates(['applied', 'skills-test', 'phone-screen', 'open', 'interview', 'selected', 'offered'], payload.value.userId) + logger.info({ + component: 'JobCandidateEventHandler', + context: 'withDrawnJobCandidates', + message: `Finished update jobCandidates as ${payload.value.id} candidate` + }) + } else { + logger.debug({ + component: 'JobCandidateEventHandler', + context: 'withDrawnJobCandidates', + message: `id: ${payload.value.id} candidate is not a placed gig requiring 20 hrs per week` + }) + } + } +} + /** * Process job candidate create event. * @@ -62,6 +143,7 @@ async function processCreate (payload) { */ async function processUpdate (payload) { await inReviewJob(payload) + await withDrawnJobCandidates(payload) } module.exports = { From 3bd9d5aa8898cc48f900438c7391f2df1d85fd32 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Wed, 21 Jul 2021 20:22:14 +0800 Subject: [PATCH 19/25] adjust code comments --- src/eventHandlers/JobCandidateEventHandler.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/eventHandlers/JobCandidateEventHandler.js b/src/eventHandlers/JobCandidateEventHandler.js index a964d459..eeb02411 100644 --- a/src/eventHandlers/JobCandidateEventHandler.js +++ b/src/eventHandlers/JobCandidateEventHandler.js @@ -70,7 +70,7 @@ async function updateJobCandidates (statuses, userId) { logger.info({ component: 'JobCandidateEventHandler', context: 'updateJobCandidates', - message: `Begining update id: ${candidate.id}' candidate with ${candidate.status} status into ${config.STATUS_MAPPING[candidate.status]} for userId: ${userId}` + message: `Begin update id: ${candidate.id}' candidate with ${candidate.status} status into ${config.STATUS_MAPPING[candidate.status]} for userId: ${userId}` }) await JobCandidateService.partiallyUpdateJobCandidate( helper.getAuditM2Muser(), @@ -80,7 +80,7 @@ async function updateJobCandidates (statuses, userId) { logger.info({ component: 'JobCandidateEventHandler', context: 'updateJobCandidates', - message: `Finishing update id: ${result.id}' candidate into ${result.status} status for userId: ${userId}` + message: `Finish update id: ${result.id}' candidate into ${result.status} status for userId: ${userId}` }) }) }) @@ -107,19 +107,19 @@ async function withDrawnJobCandidates (payload) { logger.info({ component: 'JobCandidateEventHandler', context: 'withDrawnJobCandidates', - message: `Begining update jobCandidates as ${payload.value.id} candidate's new gig is requiring 20 hrs per week` + message: `Begin update jobCandidates as ${payload.value.id} candidate's new gig is requiring more than 20 hrs per week` }) await updateJobCandidates(['applied', 'skills-test', 'phone-screen', 'open', 'interview', 'selected', 'offered'], payload.value.userId) logger.info({ component: 'JobCandidateEventHandler', context: 'withDrawnJobCandidates', - message: `Finished update jobCandidates as ${payload.value.id} candidate` + message: `Finish update jobCandidates as ${payload.value.id} candidate` }) } else { logger.debug({ component: 'JobCandidateEventHandler', context: 'withDrawnJobCandidates', - message: `id: ${payload.value.id} candidate is not a placed gig requiring 20 hrs per week` + message: `id: ${payload.value.id} candidate is not placing on a gig requiring 20 hrs per week` }) } } From 03981674ab81473841a372fc1fedab103df1e134 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 21 Jul 2021 18:31:03 +0300 Subject: [PATCH 20/25] fix: local config --- .gitignore | 1 + README.md | 18 ++++++++++++++++-- local/docker-compose.yml | 5 ++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 69ec29ae..ea148e00 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,7 @@ web_modules/ # dotenv environment variables file .env .env.test +taas-es-processor.env # parcel-bundler cache (https://parceljs.org/) .cache diff --git a/README.md b/README.md index 287bf7cb..c7849faf 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,21 @@ BUSAPI_URL=http://dockerhost:8002/v5 ``` - - Values from this file would be automatically used by many `npm` commands. + 1. In the `./local` folder create `taas-es-processor.env` file with the next environment variables.
+ + ```bash + # Auth0 config + AUTH0_URL= + AUTH0_AUDIENCE= + AUTH0_CLIENT_ID= + AUTH0_CLIENT_SECRET= + # Locally deployed services (via docker-compose) + KAFKA_URL=kafka:9093 + ES_HOST=http://elasticsearch:9200 + BUSAPI_URL=http://tc-bus-api:8002/v5 + ``` + + - Values from these file would be automatically used by many `npm` commands. - ⚠️ Never commit this file or its copy to the repository! 1. Set `dockerhost` to point the IP address of Docker. Docker IP address depends on your system. For example if docker is run on IP `127.0.0.1` add a the next line to your `/etc/hosts` file: @@ -220,7 +234,7 @@ To be able to change and test `taas-es-processor` locally you can follow the nex | `npm run cov` | Code Coverage Report. | | `npm run migrate` | Run any migration files which haven't run yet. | | `npm run migrate:undo` | Revert most recent migration. | -| `npm run demo-payment-scheduler` | Create 1000 Work Periods Payment records in with status "scheduled" and various "amount" | +| `npm run demo-payment-scheduler` | Create 1000 Work Periods Payment records in with status "scheduled" and various "amount" | ## Import and Export data diff --git a/local/docker-compose.yml b/local/docker-compose.yml index 34229bde..e7ea5a7f 100644 --- a/local/docker-compose.yml +++ b/local/docker-compose.yml @@ -58,9 +58,8 @@ services: depends_on: - kafka-client - elasticsearch - environment: - - KAFKA_URL=kafka:9093 - - ES_HOST=http://elasticsearch:9200 + env_file: + - taas-es-processor.env tc-bus-api: container_name: tc-bus-api From 77c1eebd4c455fb00595550b133b4dcae270b31d Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Wed, 21 Jul 2021 23:46:43 +0800 Subject: [PATCH 21/25] update naming --- config/default.js | 4 ++-- src/eventHandlers/JobCandidateEventHandler.js | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/config/default.js b/config/default.js index 862f71e9..cb589290 100644 --- a/config/default.js +++ b/config/default.js @@ -217,8 +217,8 @@ module.exports = { }, // if a job reach this critier, system will automatically withdrawn other job applications. JOBS_HOUR_PER_WEEK: 20, - // status mapping - STATUS_MAPPING: { + // the mapping includes the status transformation when auto-withdrawn feature is performed on job candidates. + WITHDRAWN_STATUS_CHANGE_MAPPING: { applied: 'withdrawn-prescreen', 'skills-test': 'withdrawn-prescreen', 'phone-screen': 'withdrawn-prescreen', diff --git a/src/eventHandlers/JobCandidateEventHandler.js b/src/eventHandlers/JobCandidateEventHandler.js index eeb02411..e42168f1 100644 --- a/src/eventHandlers/JobCandidateEventHandler.js +++ b/src/eventHandlers/JobCandidateEventHandler.js @@ -70,12 +70,12 @@ async function updateJobCandidates (statuses, userId) { logger.info({ component: 'JobCandidateEventHandler', context: 'updateJobCandidates', - message: `Begin update id: ${candidate.id}' candidate with ${candidate.status} status into ${config.STATUS_MAPPING[candidate.status]} for userId: ${userId}` + message: `Begin update id: ${candidate.id}' candidate with ${candidate.status} status into ${config.WITHDRAWN_STATUS_CHANGE_MAPPING[candidate.status]} for userId: ${userId}` }) await JobCandidateService.partiallyUpdateJobCandidate( helper.getAuditM2Muser(), candidate.id, - { status: config.STATUS_MAPPING[candidate.status] } + { status: config.WITHDRAWN_STATUS_CHANGE_MAPPING[candidate.status] } ).then(result => { logger.info({ component: 'JobCandidateEventHandler', @@ -133,6 +133,7 @@ async function withDrawnJobCandidates (payload) { */ async function processCreate (payload) { await inReviewJob(payload) + await withDrawnJobCandidates(payload) } /** From d1ee8843cb48236abdc7c4354b7ad2fafb6f85c6 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Thu, 22 Jul 2021 00:12:06 +0800 Subject: [PATCH 22/25] add futher logic --- src/eventHandlers/JobCandidateEventHandler.js | 8 ++++++-- src/services/JobCandidateService.js | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/eventHandlers/JobCandidateEventHandler.js b/src/eventHandlers/JobCandidateEventHandler.js index e42168f1..2ad9005b 100644 --- a/src/eventHandlers/JobCandidateEventHandler.js +++ b/src/eventHandlers/JobCandidateEventHandler.js @@ -133,7 +133,9 @@ async function withDrawnJobCandidates (payload) { */ async function processCreate (payload) { await inReviewJob(payload) - await withDrawnJobCandidates(payload) + if (payload.value.status === 'placed') { + await withDrawnJobCandidates(payload) + } } /** @@ -144,7 +146,9 @@ async function processCreate (payload) { */ async function processUpdate (payload) { await inReviewJob(payload) - await withDrawnJobCandidates(payload) + if (payload.value.status === 'placed' && payload.options.oldValue.status !== 'placed') { + await withDrawnJobCandidates(payload) + } } module.exports = { diff --git a/src/services/JobCandidateService.js b/src/services/JobCandidateService.js index b08922e4..8bdd2b60 100644 --- a/src/services/JobCandidateService.js +++ b/src/services/JobCandidateService.js @@ -144,6 +144,7 @@ createJobCandidate.schema = Joi.object().keys({ */ async function updateJobCandidate (currentUser, id, data) { const jobCandidate = await JobCandidate.findById(id) + const oldValue = jobCandidate.toJSON() const userId = await helper.getUserId(currentUser.userId) // check user permission @@ -155,7 +156,7 @@ async function updateJobCandidate (currentUser, id, data) { data.updatedBy = userId const updated = await jobCandidate.update(data) - await helper.postEvent(config.TAAS_JOB_CANDIDATE_UPDATE_TOPIC, updated.toJSON()) + await helper.postEvent(config.TAAS_JOB_CANDIDATE_UPDATE_TOPIC, updated.toJSON(), { oldValue: oldValue }) const result = _.assign(jobCandidate.dataValues, data) return result } From bca7f605495872051a6480ee0370c3a6e96014a5 Mon Sep 17 00:00:00 2001 From: yoution Date: Thu, 22 Jul 2021 10:25:36 +0800 Subject: [PATCH 23/25] fix: issue #370 mapping emsi tags --- README.md | 7 + package.json | 3 +- scripts/emsi-mapping/esmi-skills-mapping.js | 419 ++++++++++++++++++++ scripts/emsi-mapping/index.js | 52 +++ src/services/TeamService.js | 48 ++- 5 files changed, 503 insertions(+), 26 deletions(-) create mode 100644 scripts/emsi-mapping/esmi-skills-mapping.js create mode 100644 scripts/emsi-mapping/index.js diff --git a/README.md b/README.md index 287bf7cb..458b4d3a 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,8 @@ To be able to change and test `taas-es-processor` locally you can follow the nex | `npm run migrate` | Run any migration files which haven't run yet. | | `npm run migrate:undo` | Revert most recent migration. | | `npm run demo-payment-scheduler` | Create 1000 Work Periods Payment records in with status "scheduled" and various "amount" | +| `npm run emsi-mapping` | mapping EMSI tags to topcoder skills | + ## Import and Export data @@ -334,3 +336,8 @@ When we add, update or delete models and/or endpoints we have to make sure that - **DB Migration** - If there are any updates in DB schemas, create a DB migration script inside `migrations` folder which would make any necessary updates to the DB schema. - Test, that when we migrate DB from the previous state using `npm run migrate`, we get exactly the same DB schema as if we create DB from scratch using command `npm run init-db force`. + +## EMSI mapping +mapping EMSI tags to topcoder skills +Run `npm run emsi-mapping` to create the mapping file +It will take about 15 minutes to create the mapping file `script/emsi-mapping/emsi-skils-mapping.js` diff --git a/package.json b/package.json index 3e2e7b18..17c1887b 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "init-db": "node src/init-db.js", "create-index": "node scripts/es/createIndex.js", "delete-index": "node scripts/es/deleteIndex.js", + "emsi-mapping": "node scripts/emsi-mapping/index.js", "index:all": "node scripts/es/reIndexAll.js", "index:jobs": "node scripts/es/reIndexJobs.js", "index:job-candidates": "node scripts/es/reIndexJobCandidates.js", @@ -87,4 +88,4 @@ "test/unit/**" ] } -} \ No newline at end of file +} diff --git a/scripts/emsi-mapping/esmi-skills-mapping.js b/scripts/emsi-mapping/esmi-skills-mapping.js new file mode 100644 index 00000000..4f070178 --- /dev/null +++ b/scripts/emsi-mapping/esmi-skills-mapping.js @@ -0,0 +1,419 @@ +module.exports = { + matchedSkills: { + Dropwizard: 'Dropwizard', + Nginx: 'NGINX', + 'Machine Learning': 'Machine Learning', + 'Force.Com': 'Force.Com Sites', + 'User Interface': 'UI Prototype', + Docker: 'Docker', + Appcelerator: 'appcelerator', + Flux: 'Flux', + 'Bootstrap (FRONT-END FRAMEWORK)': 'Twitter Bootstrap', + Financialforce: 'FinancialForce', + Redis: 'Redis', + Hybris: 'Hybris', + Splunk: 'Splunk', + 'Lua (SCRIPTING LANGUAGE)': 'Lua', + 'Jface (UI TOOLKIT)': 'Jface', + Recursion: 'Recursion', + Blackberry: 'Blackberry SDK', + Xul: 'XUL', + Mapreduce: 'MapReduce', + Nosql: 'NoSQL', + Linux: 'Linux', + Elasticsearch: 'Elasticsearch', + 'Microsoft Silverlight': 'Microsoft Silverlight', + Vertica: 'Vertica', + 'Windows Servers': 'Windows Server', + 'Haskell (PROGRAMMING LANGUAGE)': 'Haskell', + Hyperledger: 'Hyperledger', + 'Apache Cordova': 'Apache Cordova', + 'Play Framework': 'Play Framework', + Zipkin: 'Zipkin', + Marklogic: 'MarkLogic', + Mysql: 'MySql', + Visualforce: 'Visualforce', + 'Data Architecture': 'IBM Rational Data Architect', + 'Windows Communication Foundation': 'Windows Communication Foundation', + 'Jboss Seam': 'JBoss Seam', + 'Java Stored Procedure (SQL)': 'Transact-SQL', + 'Component Object Model (COM)': 'COM', + 'Ubuntu (OPERATING SYSTEM)': 'ubuntu', + 'Cobol (PROGRAMMING LANGUAGE)': 'Cobol', + 'Continuous Integration': 'Continuous Integration', + 'Extensible Messaging And Presence Protocol (XMPP)': 'XMPP', + Microservices: 'Microservices', + 'Java Platform Micro Edition (J2ME)': 'J2ME', + 'Qt (SOFTWARE)': 'Qt', + 'R (PROGRAMMING LANGUAGE)': 'R', + 'Scala (PROGRAMMING LANGUAGE)': 'Scala', + 'Dynamic Programming': 'Dynamic Programming', + 'C (PROGRAMMING LANGUAGE)': 'C#', + Typescript: 'TypeScript', + Xamarin: 'Xamarin', + 'Sql Server Integration Services (SSIS)': 'SSIS', + Kubernetes: 'Kubernetes', + Inkscape: 'Inkscape', + 'Ibm Websphere Portal': 'IBM WebSphere Portal', + Matlab: 'Matlab', + Jekyll: 'Jekyll', + Cassandra: 'Cassandra', + 'Airplay Sdk (APPLE)': 'Apple HIG', + Jquery: 'jQuery Mobile', + 'Power Bi': 'Power BI', + Json: 'JSON', + 'Django (WEB FRAMEWORK)': 'Django', + 'Meteor.Js': 'Meteor.js', + Clojure: 'Clojure', + 'App Store (IOS)': 'iOS', + 'Amazon Alexa': 'Amazon Alexa', + 'Ibm Bluemix': 'IBM Bluemix', + 'Extensible Stylesheet Language (XSL)': 'XSL', + 'React.Js': 'React.js', + Gradle: 'Gradle', + Protractor: 'Protractor', + 'Java Platform Enterprise Edition (J2EE)': 'J2EE', + Drupal: 'Drupal', + 'Php (SCRIPTING LANGUAGE)': 'PHP', + 'Customer Experience': 'Customer Experience (Cx)', + Mariadb: 'MariaDB', + Grommet: 'Grommet', + Clickonce: 'ClickOnce', + 'Application Programming Interface (API)': 'API', + 'Unit Testing': 'Unit-Testing', + 'Ionic Framework': 'Ionic Framework', + Moodle: 'moodle', + Jbehave: 'JBehave', + Gremlin: 'Gremlin', + Office365: 'Office365', + 'Fortran (PROGRAMMING LANGUAGE)': 'Fortran', + 'Vue.Js': 'Vuejs', + 'Google Maps': 'Google-Maps', + 'Cloud Foundry': 'Cloud Foundry', + 'Robot Framework': 'Robot Framework', + Ethereum: 'Ethereum', + Neo4J: 'Neo4J', + 'Microsoft Dynamics': 'Microsoft Dynamics', + 'Geospatial Information Technology (GIT)': 'Git', + Predix: 'Predix', + Gitlab: 'Gitlab', + 'Windows Workflow Foundation': 'Windows Workflow Foundation', + 'Javascript (PROGRAMMING LANGUAGE)': 'JavaScript', + 'Backbone.Js': 'Backbone.js', + Jabber: 'Jabber', + Wordpress: 'Wordpress', + Devops: 'DevOps', + 'Apache Derby': 'Apache Derby', + 'Rexx (PROGRAMMING LANGUAGE)': 'IBM REXX', + 'Web Scraping': 'Web scraping', + Sorting: 'Sorting', + 'Message Broker': 'IBM Websphere Message Broker', + Openam: 'Openam', + Less: 'Less', + 'Equinox (OSGI)': 'OSGi', + 'Zend Framework': 'zend framework', + 'Sketch (DESIGN SOFTWARE)': 'Sketch', + Coffeescript: 'Coffeescript', + 'Gnu Image Manipulation Program (GIMP)': 'gimp', + 'Node.Js': 'Node.js', + Laravel: 'laravel', + 'Ruby (PROGRAMMING LANGUAGE)': 'Ruby', + Mongodb: 'MongoDB', + 'Graphic Design': 'Graphic Design', + 'Entity Framework': 'Entity-Framework', + 'Hibernate (JAVA)': 'Hibernate', + 'Data Visualization': 'Data Visualization', + 'Windows Phone': 'Windows Phone', + 'Bash (SCRIPTING LANGUAGE)': 'Bash', + 'Akka (TOOLKIT)': 'Akka', + 'Sencha Touch': 'Sencha Touch 2', + Multithreading: 'Multithreading', + Apigee: 'Apigee', + 'Iso/Iec 14882 (C++)': 'C++', + 'Ab Initio (SOFTWARE)': 'Ab Initio', + 'Python (PROGRAMMING LANGUAGE)': 'Python', + 'Big Data': 'Big data', + Vscode: 'VSCode', + Codeigniter: 'Codeigniter', + 'Grunt.Js': 'Grunt.js', + 'Swing (DANCE)': 'Swing', + 'Groovy (PROGRAMMING LANGUAGE)': 'Groovy', + Openshift: 'OpenShift', + Integration: 'IBM Integration Bus', + Compression: 'Compression', + 'Salesforce.Com': 'Salesforce.com', + 'Ibm Websphere Mq': 'IBM WebSphere MQ', + 'Information Architecture': 'Information Architecture (IA)', + 'Ember.Js': 'Ember.js', + 'Vim (TEXT EDITOR)': 'vim', + Html5: 'HTML5', + 'Custom Tag': 'Custom Tag', + 'Asp.Net': 'ASP.NET', + 'Responsive Web Design': 'Responsive Web Design', + 'Ibm Rational Software': 'IBM Rational Software Architect', + Corda: 'R3 Corda', + Phonegap: 'Phonegap', + Junit: 'Junit', + 'Graph Theory': 'Graph Theory', + 'Eclipse (SOFTWARE)': 'Eclipse', + Bigquery: 'BigQuery', + Requirejs: 'Require.js', + Flash: 'Flash', + Github: 'Github', + 'Cascading Style Sheets (CSS)': 'CSS', + 'Web Services': 'Web Services', + Phantomjs: 'Phantomjs', + Heroku: 'Heroku', + Geometry: 'Geometry', + 'Java Message Service (JMS)': 'JMS', + 'Aws Lambda': 'AWS Lambda', + Sass: 'SASS', + 'Artificial Intelligence': 'AI', + Talend: 'Talend', + Quorum: 'Quorum', + Kotlin: 'Kotlin', + 'Google Cloud': 'Google Cloud', + 'Interaction Design': 'Interaction Design (Ixd)', + Sqlite: 'Sqlite', + Postgresql: 'PostgreSQL', + 'User Experience': 'User Experience (Ux)', + Invision: 'InVision', + 'Vert.X': 'Vert.X', + Oauth: 'Oauth', + Smartsheet: 'Smartsheet', + Actionscript: 'ActionScript', + Drools: 'Drools', + 'Apache Kafka': 'Apache Kafka', + 'Perl (PROGRAMMING LANGUAGE)': 'Perl', + Parsing: 'String Parsing', + 'Product Design': 'Product Design', + Openstack: 'Openstack', + 'Android (OPERATING SYSTEM)': 'Android', + 'Google App Engines': 'Google App Engine', + 'Apache Camel': 'Apache Camel', + 'Java (PROGRAMMING LANGUAGE)': 'Java', + 'Application Servers': 'IBM Websphere Application Server', + 'Hypertext Markup Language (HTML)': 'HTML', + 'Sitemaps (XML)': 'XML', + Clojurescript: 'ClojureScript', + Blockchain: 'Blockchain', + Cartodb: 'CartoDB', + 'Oracle Databases': 'Oracle Database', + 'Ibm Lotus Domino': 'IBM Lotus Domino', + Indexeddb: 'IndexedDB', + 'Data Science': 'Data Science', + 'Ajax (PROGRAMMING LANGUAGE)': 'Ajax', + Twilio: 'Twilio', + Selenium: 'Selenium', + Trello: 'trello', + Appium: 'Appium', + Jruby: 'Jruby', + 'Ibm Db2': 'IBM DB2', + Branding: 'Branding', + '3D Reconstruction': '3D Reconstruction', + 'Ibm Aix': 'IBM AiX', + 'Active Directory': 'Active Directory' + }, + unMatchedSkills: [ + 'EJB', + 'Database', + 'Winforms', + 'Photoshop', + '.NET', + 'Leaflet.js', + 'Databasedotcom', + 'Maven', + 'Gaming', + 'Go', + 'Mobile', + 'IBM WebSphere DataStage', + 'Azure', + 'Om', + 'Lightning', + 'File', + 'Security', + 'Tableau', + 'Ibatis/Mybatis', + 'Integrator', + 'HAML', + 'SFDC Apex', + 'Responsive Design', + 'Castor', + 'Npm', + 'ipfs', + '.NET System.Addins', + 'TIBCO', + 'Boomi', + 'InDesign', + 'EC2', + 'Concept Design', + 'nodewebkit', + 'S3', + 'Mozilla', + 'sympfony', + 'Website Design', + 'Chatter', + 'Calabash', + 'Sinatra', + 'Algorithm', + 'OSx', + 'Open Source', + 'Frontend', + 'XAML', + 'VB', + 'Winforms Controls', + 'User Testing', + 'SFDC Lightening Components', + 'Forms', + 'Contentful', + 'bower', + 'Use Case Diagrams (TcUML)', + 'BizTalk', + 'Infographic', + 'Gulp', + 'Xcode', + 'Word/Rich Text', + 'Spring', + 'RMI', + 'OmniGraffle', + 'Linq', + 'Swift', + 'MESH01', + 'MSMQ', + 'yii', + 'IBM Rational Application Developer', + 'Illustrator', + 'QlikView', + 'MIDP 2.0', + 'Beanstalk', + 'JPA', + 'SWT', + 'Simulation', + 'Brute Force', + 'IBM Pl/1', + 'Cumulocity', + 'Windows', + 'IBM Cognitive', + 'Validation', + 'IDOL OnDemand', + 'Wpf', + 'Hadoop', + 'Search', + 'Actian Database', + 'Simple Math', + 'Box', + 'CSS3', + 'LoadRunner', + 'Sharepoint 3.0', + 'IBM COGNOS', + 'Dc.js', + 'Pl/Sql', + 'Cisco', + 'Web methods', + 'Aris', + 'Remoting', + 'Apex', + 'VB.NET', + 'PowerShell', + 'Q & Bluebird', + 'Microsoft Exchange', + 'Swagger', + 'Regex', + 'UML', + 'JSF', + 'WCF', + 'Zepto.js', + 'Flight.js', + 'Apache Flume', + 'IBM Cloud Private', + 'Activity Diagrams (Tcuml)', + 'Servlet', + 'Cocoa', + 'Greedy', + 'IBM Rational Team Concert', + 'DocuSign', + 'VBA', + 'AngularJS', + 'Mobile Design', + 'Actian Data', + 'doctrine', + 'JSP', + 'foundation', + 'Axure', + 'Knockout', + 'F#', + 'IBM Watson', + 'Excel', + 'Sockets', + 'Siebel', + 'QA', + 'UITableView', + 'Dynamodb', + 'Solidity', + 'Logo', + 'travis', + 'Visual-Studio', + 'Espruino', + 'REST', + 'Hashgraph', + 'tvOS', + 'atom', + 'Titanium', + 'Shell', + 'Tosca', + 'Ldap', + 'kraken.js', + 'Performance', + 'JDBC', + 'D3.JS', + 'Couchbase', + 'CloudFactor', + 'HTTP', + 'ADO.NET', + 'Dojo', + 'Applet', + 'Spark', + 'AWS', + 'Mainframe', + 'Facebook', + 'jetbrains', + 'Flex', + 'Ant', + 'SFDC Mobile', + 'HPE Haven OnDemand', + 'Oracle', + 'JavaBean', + 'Salesforce', + 'Struts', + 'Function', + 'Class', + 'IBM Lotus Notes', + 'SCSS', + 'Brivo Labs', + 'SAP', + 'Multichain', + 'List', + 'Express', + 'gulp', + 'JMeter', + 'Math', + 'Image', + 'Commerce Server 2009', + 'IBM Design', + 'Print', + 'Advanced Math', + 'SFDC REST APIs', + 'String Manipulation', + 'chrome', + 'String', + 'SFDC Design', + 'CA', + 'Oracle EBS', + 'Golang', + 'Simple Search', + 'Pega', + 'Cognitive', + 'redhat', + 'Marvel - Design' + ] +} diff --git a/scripts/emsi-mapping/index.js b/scripts/emsi-mapping/index.js new file mode 100644 index 00000000..58245fd4 --- /dev/null +++ b/scripts/emsi-mapping/index.js @@ -0,0 +1,52 @@ +/** + * mapping emsi skills to topcoder skills + */ + +const fs = require('fs') +const path = require('path') +const logger = require('../../src/common/logger') +const helper = require('../../src/common/helper') + +async function mappingSkill () { + const matchedSkills = {} + const unMatchedSkills = [] + const failedSkills = [] + let tcSkills + const startTime = Date.now() + try { + tcSkills = await helper.getAllTopcoderSkills() + } catch (e) { + logger.error({ component: 'getAllTopcoderSkills', context: 'emsi-mapping', message: JSON.stringify(e) }) + } + + for (let i = 0; i < tcSkills.length; i++) { + const tcSkill = tcSkills[i] + let emsiTags + try { + emsiTags = await helper.getTags(tcSkill.name) + } catch (e) { + failedSkills.push(tcSkill.name) + logger.error({ component: 'getTags', context: 'emsi-mapping', message: JSON.stringify(e) }) + } + if (emsiTags.length) { + matchedSkills[emsiTags[0].tag] = tcSkill.name + } else { + unMatchedSkills.push(tcSkill.name) + } + } + + const textString = `module.exports = { matchedSkills: ${JSON.stringify(matchedSkills, 2, 3)}, unMatchedSkills: ${JSON.stringify(unMatchedSkills, 2, 2)} }` + const filePath = path.join(__dirname, 'emsi-skills-mapping.js') + const result = { + totalTime: (Date.now() - startTime) / 60 / 1000 + ' min', + totalSkills: tcSkills.length, + matchedSkills: tcSkills.length - unMatchedSkills.length, + unMatchedSkills: unMatchedSkills.length, + filePath, + failSkills: failedSkills + } + + logger.info({ component: 'emsi-mapping', context: 'emsi-mapping', message: JSON.stringify(result) }) + fs.writeFileSync(filePath, textString) +} +mappingSkill() diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 375f5c2f..84b43d1f 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -17,9 +17,9 @@ const { Op } = require('sequelize') const models = require('../models') const stopWords = require('../../data/stopWords.json') const { getAuditM2Muser } = require('../common/helper') +const { matchedSkills, unMatchedSkills } = require('../../scripts/emsi-mapping/esmi-skills-mapping') const Role = models.Role const RoleSearchRequest = models.RoleSearchRequest -const topcoderSkills = {} const emailTemplates = _.mapValues(emailTemplateConfig, (template) => { return { @@ -64,29 +64,19 @@ async function _getJobsByProjectIds (currentUser, projectIds) { } /** - * Gets topcoder skills and stores their name and compiled - * regex patters according to Levenshtein distance <=1 + * compiled regex patters according to Levenshtein distance <=1 for unmatched skills from EMSI + * @returns {Array} the unMatched skills with regex pattern */ -async function _reloadCachedTopcoderSkills () { - // do not reload if cache time is not expired - if (!_.isUndefined(topcoderSkills.time)) { - const cacheTime = config.TOPCODER_SKILLS_CACHE_TIME * 60 * 1000 - if (new Date().getTime() - topcoderSkills.time < cacheTime) { - return - } - } - // collect all skills - const skills = await helper.getAllTopcoderSkills() - // set the last cached time - topcoderSkills.time = new Date().getTime() - topcoderSkills.skills = [] +function compileRegexPatternForNoEmsiSkills () { + const unMatched = [] // store skill names and compiled regex paterns - _.each(skills, skill => { - topcoderSkills.skills.push({ - name: skill.name, - pattern: _compileRegexPatternForSkillName(skill.name) + _.each(unMatchedSkills, skill => { + unMatched.push({ + name: skill, + pattern: _compileRegexPatternForSkillName(skill) }) }) + return unMatched } /** @@ -835,18 +825,17 @@ getRoleBySkills.schema = Joi.object() }).required() /** - * Return skills by job description. + * Return skills by job description from EMSI. * * @param {Object} currentUser the user who perform this operation. * @param {Object} data the search criteria * @returns {Object} the result */ async function getSkillsByJobDescription (data) { - // load topcoder skills if needed. Using cached skills helps to avoid - // unnecessary api calls which is extremely time comsuming. - await _reloadCachedTopcoderSkills() // replace markdown tags with spaces const description = helper.removeTextFormatting(data.description) + // get skill from emsi + const emsiTags = await helper.getTags(description) // extract words from description let words = _.split(description, ' ') // remove stopwords from description @@ -858,11 +847,20 @@ async function getSkillsByJobDescription (data) { } words = _.concat(words, twoWords) let foundSkills = [] + // add emsi parsed skills + _.each(emsiTags, (t) => { + if (matchedSkills[t.tag]) { + foundSkills.push(matchedSkills[t.tag]) + } + }) + + // unmatctched skill + const unMatchedTopcoderSkills = compileRegexPatternForNoEmsiSkills() const result = [] // try to match each word with skill names // using pre-compiled regex pattern _.each(words, word => { - _.each(topcoderSkills.skills, skill => { + _.each(unMatchedTopcoderSkills, skill => { // do not stop searching after a match in order to detect more lookalikes if (skill.pattern.test(word)) { foundSkills.push(skill.name) From 3d66ee12f1436b2208832c156c2397bdbcddd151 Mon Sep 17 00:00:00 2001 From: yoution Date: Thu, 22 Jul 2021 16:31:45 +0800 Subject: [PATCH 24/25] fix: issue #370 --- src/services/TeamService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/TeamService.js b/src/services/TeamService.js index 84b43d1f..2ffd9c2d 100644 --- a/src/services/TeamService.js +++ b/src/services/TeamService.js @@ -72,8 +72,8 @@ function compileRegexPatternForNoEmsiSkills () { // store skill names and compiled regex paterns _.each(unMatchedSkills, skill => { unMatched.push({ - name: skill, - pattern: _compileRegexPatternForSkillName(skill) + name: skill.toLowerCase(), + pattern: _compileRegexPatternForSkillName(skill.toLowerCase()) }) }) return unMatched From 721609da2fd34c1fc9d734b59aba6d5e04ad80f5 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Thu, 22 Jul 2021 17:55:35 +0300 Subject: [PATCH 25/25] use single env file --- README.md | 20 +++----------------- local/docker-compose.yml | 10 ++++++++-- package.json | 2 +- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index be6ef755..a61b99dd 100644 --- a/README.md +++ b/README.md @@ -49,21 +49,7 @@ BUSAPI_URL=http://dockerhost:8002/v5 ``` - 1. In the `./local` folder create `taas-es-processor.env` file with the next environment variables.
- - ```bash - # Auth0 config - AUTH0_URL= - AUTH0_AUDIENCE= - AUTH0_CLIENT_ID= - AUTH0_CLIENT_SECRET= - # Locally deployed services (via docker-compose) - KAFKA_URL=kafka:9093 - ES_HOST=http://elasticsearch:9200 - BUSAPI_URL=http://tc-bus-api:8002/v5 - ``` - - - Values from these file would be automatically used by many `npm` commands. + - Values from this file would be automatically used by many `npm` commands. - ⚠️ Never commit this file or its copy to the repository! 1. Set `dockerhost` to point the IP address of Docker. Docker IP address depends on your system. For example if docker is run on IP `127.0.0.1` add a the next line to your `/etc/hosts` file: @@ -234,8 +220,8 @@ To be able to change and test `taas-es-processor` locally you can follow the nex | `npm run cov` | Code Coverage Report. | | `npm run migrate` | Run any migration files which haven't run yet. | | `npm run migrate:undo` | Revert most recent migration. | -| `npm run demo-payment-scheduler` | Create 1000 Work Periods Payment records in with status "scheduled" and various "amount" | -| `npm run emsi-mapping` | mapping EMSI tags to topcoder skills | +| `npm run demo-payment-scheduler` | Create 1000 Work Periods Payment records in with status "scheduled" and various "amount" | +| `npm run emsi-mapping` | mapping EMSI tags to topcoder skills | ## Import and Export data diff --git a/local/docker-compose.yml b/local/docker-compose.yml index e7ea5a7f..e8db578a 100644 --- a/local/docker-compose.yml +++ b/local/docker-compose.yml @@ -58,8 +58,14 @@ services: depends_on: - kafka-client - elasticsearch - env_file: - - taas-es-processor.env + environment: + - KAFKA_URL=kafka:9093 + - ES_HOST=http://elasticsearch:9200 + - BUSAPI_URL=http://tc-bus-api:8002/v5 + - AUTH0_URL=${AUTH0_URL} + - AUTH0_AUDIENCE=${AUTH0_AUDIENCE} + - AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID} + - AUTH0_CLIENT_SECRET=${AUTH0_CLIENT_SECRET} tc-bus-api: container_name: tc-bus-api diff --git a/package.json b/package.json index 17c1887b..31009f5f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "migrate": "npx sequelize db:migrate", "migrate:undo": "npx sequelize db:migrate:undo", "test": "mocha test/unit/*.test.js --timeout 30000 --require test/prepare.js --exit", - "services:up": "docker-compose -f ./local/docker-compose.yml up -d", + "services:up": "docker-compose -f ./local/docker-compose.yml --env-file .env up -d", "services:down": "docker-compose -f ./local/docker-compose.yml down", "services:logs": "docker-compose -f ./local/docker-compose.yml logs", "local:init": "npm run local:reset && npm run data:import -- --force",