diff --git a/.circleci/config.yml b/.circleci/config.yml index cc1b503..eb36dfb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,17 +8,19 @@ parameters: type: boolean defaults: &defaults docker: - - image: circleci/python:2.7-stretch-browsers + - image: cimg/python:3.11.0-browsers install_dependency: &install_dependency name: Installation of build and deployment dependencies. command: | - sudo apt install jq python3-pip + sudo apt update + sudo apt install jq + sudo apt install python3-pip sudo pip3 install awscli --upgrade - # sudo pip install docker-compose + sudo pip3 install docker-compose install_deploysuite: &install_deploysuite name: Installation of install_deploysuite. command: | - git clone --branch v1.4.14 https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript + git clone --branch v1.4.15 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 fade308..ae5a090 100644 --- a/config/default.js +++ b/config/default.js @@ -68,6 +68,16 @@ module.exports = { RESOURCE_DELETE_TOPIC: process.env.RESOURCE_DELETE_TOPIC || 'challenge.action.resource.delete', RESOURCE_ROLE_CREATE_TOPIC: process.env.RESOURCE_ROLE_CREATE_TOPIC || 'challenge.action.resource.role.create', RESOURCE_ROLE_UPDATE_TOPIC: process.env.RESOURCE_ROLE_UPDATE_TOPIC || 'challenge.action.resource.role.update', + EMAIL_NOTIFICATIN_TOPIC: process.env.EMAIL_NOTIFICATIN_TOPIC || 'external.action.email', + REGISTRATION_EMAIL: { + EMAIL_FROM: process.env.EMAIL_FROM || 'no-reply@topcoder.com', + SENDGRID_TEMPLATE_ID: process.env.SENDGRID_TEMPLATE_ID || '', + SENDGRID_TEMPLATE_ID_NO_FORUM: process.env.SENDGRID_TEMPLATE_ID_NO_FORUM || '', + SUBMIT_URL: process.env.SUBMIT_URL || 'https://www.topcoder.com/challenges/:id/submit/', + REVIEW_APP_URL: process.env.REVIEW_APP_URL || 'https://software.topcoder.com/review/actions/ViewProjectDetails?pid=', + HELP_URL: process.env.HELP_URL || 'https://help.topcoder.com', + SUPPORT_EMAIL: process.env.SUPPORT_EMAIL || 'support@topcoder.com' + }, AUTOMATED_TESTING_NAME_PREFIX: process.env.AUTOMATED_TESTING_NAME_PREFIX || 'POSTMANE2E-' } diff --git a/src/common/helper.js b/src/common/helper.js index 58df8c8..19aa2e8 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -176,26 +176,30 @@ async function getMemberById (id) { * @param {String} handle The member handle * @returns {Promise} */ -async function getMemberIdByHandle (handle) { +async function getMemberDetailsByHandle (handle) { try { // logger.warn(`getMemberIdByHandle ${handle}`) const profile = await MemberProfile.query('handleLower').eq(_.toLower(handle)).exec().then(r => r[0]) - return profile.userId + return { memberId: profile.userId, email: profile.email } } catch (e) { // fall back to v3 api... - logger.warn(`Get MemberID by Handle from Dynamo Failed, trying v3 Members API. Error: ${JSON.stringify(e)}`) - return getMemberIdByHandleFromV3Members(handle) + logger.warn(`Get Member by Handle from Dynamo Failed, trying v3 Members API. Error: ${JSON.stringify(e)}`) + return getMemberDetailsByHandleFromV3Members(handle) } } -async function getMemberIdByHandleFromV3Members (handle) { +async function getMemberDetailsByHandleFromV3Members (handle) { let memberId + let email try { - logger.warn(`getMemberIdByHandle ${handle} from v5`) + logger.warn(`getMemberByHandle ${handle} from v5`) const res = await getRequest(`${config.MEMBER_API_URL}/${handle}`) if (_.get(res, 'body.userId')) { memberId = String(res.body.userId) } + if (_.get(res, 'body.email')) { + email = String(res.body.email) + } // handle return from v3 API, handle and memberHandle are the same under case-insensitive condition handle = _.get(res, 'body.handle') } catch (error) { @@ -209,7 +213,7 @@ async function getMemberIdByHandleFromV3Members (handle) { throw new errors.BadRequestError(`User with handle: ${handle} doesn't exist`) } - return memberId + return { memberId, email } } /** @@ -481,7 +485,7 @@ module.exports = { wrapExpress, autoWrapExpress, getMemberInfoById, - getMemberIdByHandle, + getMemberDetailsByHandle, checkIfExists, hasAdminRole, getById, diff --git a/src/models/MemberProfile.js b/src/models/MemberProfile.js index 583c2c8..42d5bfb 100644 --- a/src/models/MemberProfile.js +++ b/src/models/MemberProfile.js @@ -23,6 +23,9 @@ const schema = new Schema({ name: 'handleLower-index', project: true } + }, + email: { + type: String } }, { diff --git a/src/services/ResourceService.js b/src/services/ResourceService.js index 80c687d..d60c839 100644 --- a/src/services/ResourceService.js +++ b/src/services/ResourceService.js @@ -245,7 +245,7 @@ async function init (currentUser, challengeId, resource, isCreated) { // get member information using v3 API const handle = resource.memberHandle - const memberId = await helper.getMemberIdByHandle(resource.memberHandle) + const { memberId, email } = await helper.getMemberDetailsByHandle(resource.memberHandle) // check if the resource is reviewer role and has already made a submission in the challenge if (resource.roleId === config.REVIEWER_RESOURCE_ROLE_ID || resource.roleId === config.ITERATIVE_REVIEWER_RESOURCE_ROLE_ID) { @@ -284,11 +284,11 @@ async function init (currentUser, challengeId, resource, isCreated) { } // skip phase dependency checks for tasks if (_.get(challenge, 'task.isTask', false)) { - return { resources, memberId, handle } + return { resources, memberId, handle, email, challenge } } // bypass phase dependency checks if the caller is an m2m/admin if (currentUser.isMachine || helper.hasAdminRole(currentUser)) { - return { resources, memberId, handle } + return { resources, memberId, handle, email, challenge } } // check phases dependencies const dependencies = await ResourceRolePhaseDependencyService.getDependencies({ resourceRoleId: resource.roleId }) @@ -316,7 +316,7 @@ async function init (currentUser, challengeId, resource, isCreated) { }) // return resources and the member id - return { resources, memberId, handle } + return { resources, memberId, handle, email, challenge } } /** @@ -331,7 +331,7 @@ async function createResource (currentUser, resource) { // handle doesn't change in current version // Seems we don't need handle auto-correction(e.g. "THomaskranitsas"->"thomaskranitsas") - const { resources, memberId } = await init(currentUser, challengeId, resource, true) + const { resources, memberId, handle, email, challenge } = await init(currentUser, challengeId, resource, true) if (_.reduce(resources, (result, r) => _.toString(r.memberId) === _.toString(memberId) && r.roleId === resource.roleId ? true : result, @@ -358,6 +358,30 @@ async function createResource (currentUser, resource) { logger.debug(`Created resource: ${JSON.stringify(_.pick(ret, payloadFields))}`) await helper.postEvent(config.RESOURCE_CREATE_TOPIC, _.pick(ret, payloadFields)) + if (!_.get(challenge, 'task.isTask', false) && resource.roleId === config.SUBMITTER_RESOURCE_ROLE_ID) { + const forumUrl = _.get(challenge, 'discussions[0].url') + let templateId = config.REGISTRATION_EMAIL.SENDGRID_TEMPLATE_ID + if (_.isUndefined(forumUrl)) { + templateId = config.REGISTRATION_EMAIL.SENDGRID_TEMPLATE_ID_NO_FORUM + } + await helper.postEvent(config.EMAIL_NOTIFICATIN_TOPIC, { + from: config.REGISTRATION_EMAIL.EMAIL_FROM, + replyTo: config.REGISTRATION_EMAIL.EMAIL_FROM, + recipients: [email], + data: { + handle, + challengeName: challenge.name, + forum: forumUrl, + submissionEndTime: new Date(_.get(_.find(challenge.phases, phase => phase.name === 'Submission'), 'scheduledEndDate')).toUTCString(), + submitUrl: _.replace(config.REGISTRATION_EMAIL.SUBMIT_URL, ':id', challengeId), + reviewAppUrl: config.REGISTRATION_EMAIL.REVIEW_APP_URL + challenge.legacyId, + helpUrl: config.REGISTRATION_EMAIL.HELP_URL, + support: config.REGISTRATION_EMAIL.SUPPORT_EMAIL + }, + sendgrid_template_id: templateId, + version: 'v3' + }) + } return ret } catch (err) {