diff --git a/src/common/constants.js b/src/common/constants.js
index 4633ff1..9ad3e4f 100644
--- a/src/common/constants.js
+++ b/src/common/constants.js
@@ -10,6 +10,19 @@
const _ = require('lodash');
const config = require('config');
+const projectTypes = {
+ app_dev: 'Full App',
+ generic: 'Work Project',
+ visual_prototype: 'Design & Prototype',
+ visual_design: 'Design',
+};
+const icons = {
+ slack: {
+ CoderBotIcon: 'https://emoji.slack-edge.com/T03R80JP7/coder-the-bot/85ae574c0c7063ef.png',
+ CoderErrorIcon: 'https://emoji.slack-edge.com/T03R80JP7/coder-error/cd2633216e7fd385.png',
+ CoderGrinningIcon: 'https://emoji.slack-edge.com/T03R80JP7/coder-grinning/a3b7f3fe9e838377.png',
+ },
+};
module.exports = {
// The event types to be consumed from the source RabbitMQ
events: {
@@ -30,7 +43,7 @@ module.exports = {
fallback: 'A project is ready to be reviewed.',
title: _.get(data, 'project.name', ''),
title_link: `https://connect.${config.get('AUTH_DOMAIN')}/projects/${data.project.id}/`,
- text: _.truncate(_.get(data, 'project.description', ''), {length: 200, separator: /,? +.,/ }),
+ text: _.truncate(_.get(data, 'project.description', ''), { length: 200, separator: /,? +.,/ }),
ts: (new Date(_.get(data, 'project.updatedAt', null))).getTime() / 1000,
fields: [
{
@@ -40,7 +53,12 @@ module.exports = {
},
{
title: 'Owner',
- value: `${_.get(data, 'owner.firstName', '')} ${_.get(data, 'owner.lastName', '')}` ,
+ value: `${_.get(data, 'owner.firstName', '')} ${_.get(data, 'owner.lastName', '')}`,
+ short: false,
+ },
+ {
+ title: 'Project Type',
+ value: projectTypes[data.project.type],
short: false,
},
],
@@ -48,16 +66,61 @@ module.exports = {
},
projectUnclaimed: (data) => {
return {
+ icon_url: icons.slack.CoderBotIcon,
channel: `${config.get('SLACK_CHANNEL_COPILOTS')}`,
pretext: 'A project has been reviewed and needs a copilot. Please check it out and claim it.',
fallback: 'A project has been reviewed and needs a copilot. Please check it out and claim it.',
title: _.get(data, 'project.name', ''),
title_link: `https://connect.${config.get('AUTH_DOMAIN')}/projects/${data.project.id}/`,
- text: _.truncate(_.get(data, 'project.description', ''), {length: 200, separator: /,? +.,/ }),
+ text: _.truncate(_.get(data, 'project.description', ''), { length: 200, separator: /,? +.,/ }),
+ ts: (new Date(_.get(data, 'project.updatedAt', null))).getTime() / 1000,
+ fields: [
+ {
+ title: 'Project Type',
+ value: projectTypes[data.project.type],
+ short: false,
+ },
+ ]
+ }
+ },
+ projectUnclaimedReposted: (data) => {
+ return {
+ icon_url: icons.slack.CoderErrorIcon,
+ channel: `${config.get('SLACK_CHANNEL_COPILOTS')}`,
+ pretext: 'We\'re still looking for a copilot for a reviewed project. Please check it out and claim it.',
+ fallback: 'We\'re still looking for a copilot for a reviewed project. Please check it out and claim it.',
+ title: _.get(data, 'project.name', ''),
+ title_link: `https://connect.${config.get('AUTH_DOMAIN')}/projects/${data.project.id}/`,
+ text: _.truncate(_.get(data, 'project.description', ''), { length: 200, separator: /,? +.,/ }),
ts: (new Date(_.get(data, 'project.updatedAt', null))).getTime() / 1000,
- fields: []
+ fields: [
+ {
+ title: 'Project Type',
+ value: projectTypes[data.project.type],
+ short: false,
+ },
+ ]
}
- }
+ },
+ projectClaimed: (data) => {
+ return {
+ icon_url: icons.slack.CoderGrinningIcon,
+ channel: `${config.get('SLACK_CHANNEL_COPILOTS')}`,
+ pretext: `${data.firstName} ${data.lastName} has claimed a project. Welcome to the team!`,
+ fallback: `${data.firstName} ${data.lastName} has claimed a project. Welcome to the team!`,
+ title: _.get(data, 'project.name', ''),
+ title_link: `https://connect.${config.get('AUTH_DOMAIN')}/projects/${data.project.id}/`,
+ text: _.truncate(_.get(data, 'project.description', ''), { length: 200, separator: /,? +.,/ }),
+ ts: (new Date(_.get(data, 'project.updatedAt', null))).getTime() / 1000,
+ fields: [
+ {
+ title: 'Project Type',
+ value: projectTypes[data.project.type],
+ short: false,
+ },
+ ]
+ }
+ },
},
discourse: {
project: {
@@ -107,6 +170,10 @@ module.exports = {
title: 'Your project has a new owner',
content: (data) => `${data.firstName} ${data.lastName} is now responsible for project ${data.projectName}. Good luck ${data.firstName}.`,
},
+ ownerAdded: {
+ title: 'Ownership changed',
+ content: (data) => `Your project has a new owner ${data.firstName} ${data.lastName} is now responsible for project Project title. Good luck ${data.firstName}!`,
+ },
},
},
project: {
@@ -138,4 +205,6 @@ module.exports = {
customer: 'customer',
copilot: 'copilot',
},
+ projectTypes,
+ icons,
};
diff --git a/src/handlers/memberEvents.js b/src/handlers/memberEvents.js
index 234eee0..e5ec2dc 100644
--- a/src/handlers/memberEvents.js
+++ b/src/handlers/memberEvents.js
@@ -10,6 +10,7 @@
const config = require('config');
const constants = require('../common/constants');
const util = require('./util');
+const _ = require('lodash');
/**
* Create notifications from project.member.added events
@@ -23,12 +24,32 @@ function* memberAdded(logger, data) {
];
let topic;
- if (data.role === constants.memberRoles.customer) {
+ const notifications = {
+ slack: {
+ copilot: [],
+ },
+ };
+ if (data.role === constants.memberRoles.customer && data.isPrimary) {
+ topic = constants.notifications.discourse.teamMembers.ownerAdded;
+ } else if (data.role === constants.memberRoles.customer) {
topic = constants.notifications.discourse.teamMembers.added;
} else if (data.role === constants.memberRoles.manager) {
topic = constants.notifications.discourse.teamMembers.managerJoined;
} else if (data.role === constants.memberRoles.copilot) {
topic = constants.notifications.discourse.teamMembers.copilotJoined;
+ // Notify project claimed
+ if ((project.status === constants.projectStatuses.active ||
+ project.status === constants.projectStatuses.reviewed)
+ && _.filter(project.members, ['role', 'copilot']).length < 2) {
+ const slackNotification = util.buildSlackNotification(
+ {
+ project,
+ firstName: addedMember.firstName,
+ lastName: addedMember.lastName,
+ },
+ constants.notifications.slack.projectClaimed);
+ notifications.slack.copilot.push(slackNotification);
+ }
}
const topicData = {
@@ -38,13 +59,11 @@ function* memberAdded(logger, data) {
lastName: addedMember.lastName,
};
- const notifications = {
- discourse: [{
- projectId: project.id,
- title: topic.title,
- content: topic.content(topicData),
- }],
- };
+ notifications.discourse = [{
+ projectId: project.id,
+ title: topic.title,
+ content: topic.content(topicData),
+ }];
return notifications;
}
diff --git a/src/handlers/projectEvents.js b/src/handlers/projectEvents.js
index 6e65c1d..b7ffbf6 100644
--- a/src/handlers/projectEvents.js
+++ b/src/handlers/projectEvents.js
@@ -129,9 +129,9 @@ function* projectUnclaimedNotifications(logger, data) {
projectCopilotIds.length === 0) {
notifications.delayed = data;
const slackNotification = util.buildSlackNotification(
- { project, },
- constants.notifications.slack.projectUnclaimed
- )
+ { project },
+ constants.notifications.slack.projectUnclaimedReposted
+ );
notifications.slack.copilot.push(slackNotification);
}
return notifications;
diff --git a/src/handlers/util.js b/src/handlers/util.js
index bb2b8df..c992503 100644
--- a/src/handlers/util.js
+++ b/src/handlers/util.js
@@ -200,7 +200,7 @@ function buildSlackNotification(data, slackDataGenerator) {
const slackData = slackDataGenerator(data);
return {
username: config.get('SLACK_USERNAME'),
- icon_url: config.get('SLACK_ICON_URL'),
+ icon_url: slackData.url || config.get('SLACK_ICON_URL'),
channel: slackData.channel,
attachments: [{
color: "#36a64f",
diff --git a/src/test/app.test.js b/src/test/app.test.js
index d4f503c..894e59a 100644
--- a/src/test/app.test.js
+++ b/src/test/app.test.js
@@ -27,6 +27,7 @@ const sampleEvents = {
updatedReviewedAnotherStatus: require('./data/events.updated.reviewed.anotherStatus.json'),
updatedReviewedSameStatus: require('./data/events.updated.reviewed.sameStatus.json'),
memberAddedTeamMember: require('./data/events.memberAdded.teamMember.json'),
+ memberAddedOwner: require('./data/events.memberAdded.owner.json'),
memberAddedManager: require('./data/events.memberAdded.manager.json'),
memberAddedCopilot: require('./data/events.memberAdded.copilot.json'),
memberRemovedLeft: require('./data/events.memberRemoved.left.json'),
@@ -55,7 +56,13 @@ const expectedSlackNotficationBase = {
title: "test",
title_link: "https://connect.topcoder-dev.com/projects/1/",
text: "test",
- fields: [],
+ fields: [
+ {
+ short: false,
+ title: 'Project Type',
+ value: 'Design',
+ },
+ ],
footer: "Topcoder",
footer_icon: "https://emoji.slack-edge.com/T03R80JP7/topcoder/7c68acd90a6b6d77.png",
ts: 1478304000,
@@ -66,7 +73,21 @@ const expectedSlackCopilotNotification = _.cloneDeep(expectedSlackNotficationBas
_.extend(expectedSlackCopilotNotification.attachments[0], {
pretext: 'A project has been reviewed and needs a copilot. Please check it out and claim it.',
fallback: 'A project has been reviewed and needs a copilot. Please check it out and claim it.',
-})
+});
+
+const expectedRepostedSlackCopilotNotification = _.cloneDeep(expectedSlackNotficationBase);
+_.extend(expectedRepostedSlackCopilotNotification.attachments[0], {
+ pretext: 'We\'re still looking for a copilot for a reviewed project. Please check it out and claim it.',
+ fallback: 'We\'re still looking for a copilot for a reviewed project. Please check it out and claim it.',
+});
+const expectedClaimedSlackCopilotNotification = _.cloneDeep(expectedSlackNotficationBase);
+_.extend(expectedClaimedSlackCopilotNotification.attachments[0], {
+ pretext: 'F_user L_user has claimed a project. Welcome to the team!',
+ fallback: 'F_user L_user has claimed a project. Welcome to the team!',
+ text: 'Project description 1',
+ title: 'Project name 1',
+ ts: '1477671612',
+});
const expectedManagerSlackNotification = _.cloneDeep(expectedSlackNotficationBase);
_.extend(expectedManagerSlackNotification.attachments[0], {
@@ -75,6 +96,11 @@ _.extend(expectedManagerSlackNotification.attachments[0], {
fields: [
{ title: 'Ref Code', value: '', short: false },
{ title: 'Owner', value: 'F_user L_user', short: false },
+ {
+ short: false,
+ title: 'Project Type',
+ value: 'Design',
+ },
]
})
@@ -278,7 +304,7 @@ describe('app', () => {
assertCount += 1;
sinon.assert.notCalled(spy);
const params = slackSpy.lastCall.args;
- assert.deepEqual(params[1], expectedSlackCopilotNotification);
+ assert.deepEqual(params[1], expectedRepostedSlackCopilotNotification);
// console.log('assert#', assertCount)
// console.log('callbackCount#', callbackCount)
// checkAssert(assertCount, callbackCount, done);
@@ -316,6 +342,18 @@ describe('app', () => {
});
describe('`project.member.added` event', () => {
+ it('should create `Project.Member.ownerAdded` notification', (done) => {
+ sendTestEvent(sampleEvents.memberAddedOwner, 'project.member.added');
+ setTimeout(() => {
+ const expectedTitle = 'Ownership changed';
+ const expectedBody = 'Your project has a new owner F_user L_user is now responsible for project Project title. Good luck F_user!';
+ const params = spy.lastCall.args;
+ assert.equal(params[2], expectedTitle);
+ assert.equal(params[3], expectedBody);
+ done();
+ }, testTimeout);
+ });
+
it('should create `Project.Member.TeamMemberAdded` notification', (done) => {
sendTestEvent(sampleEvents.memberAddedTeamMember, 'project.member.added');
setTimeout(() => {
@@ -351,6 +389,32 @@ describe('app', () => {
done();
}, testTimeout);
});
+ it('should create `Project.Member.CopilotJoined` notification and slack copilot joined notification', (done) => {
+ request.get.restore();
+ stub = sinon.stub(request, 'get');
+ stub.withArgs(sinon.match.has('url', `${config.API_BASE_URL}/v4/projects/1`))
+ .yields(null, { statusCode: 200 }, sampleProjects.projectTest);
+ stub.withArgs(sinon.match.has('url', `${config.API_BASE_URL}/v3/members/_search/?query=userId:40051331`))
+ .yields(null, { statusCode: 200 }, sampleUsers.user1);
+
+ sendTestEvent(sampleEvents.memberAddedCopilot, 'project.member.added');
+ setTimeout(() => {
+ const expectedTitle = 'A Topcoder copilot has joined your project';
+ const expectedBody = 'F_user L_user has joined your project test as a copilot.';
+ const params = spy.lastCall.args;
+ assert.equal(params[2], expectedTitle);
+ assert.equal(params[3], expectedBody);
+ const slackParams = slackSpy.lastCall.args;
+ const expectedTestCopilotNotificaton = _.cloneDeep(expectedClaimedSlackCopilotNotification);
+ _.extend(expectedTestCopilotNotificaton.attachments[0], {
+ text: 'test',
+ title: 'test',
+ ts: 1478304000,
+ });
+ assert.deepEqual(slackParams[1], expectedTestCopilotNotificaton);
+ done();
+ }, testTimeout);
+ });
});
describe('`project.member.removed` event', () => {
diff --git a/src/test/data/events.memberAdded.owner.json b/src/test/data/events.memberAdded.owner.json
new file mode 100644
index 0000000..e57eb32
--- /dev/null
+++ b/src/test/data/events.memberAdded.owner.json
@@ -0,0 +1,11 @@
+{
+ "id": 1185,
+ "userId": 40051331,
+ "role": "customer",
+ "isPrimary": true,
+ "createdAt": "2016-11-04T03:57:57.000Z",
+ "updatedAt": "2016-11-04T03:57:57.000Z",
+ "createdBy": 40152856,
+ "updatedBy": 40152856,
+ "projectId": 1
+}
diff --git a/src/test/data/events.memberAdded.teamMember.json b/src/test/data/events.memberAdded.teamMember.json
index e57eb32..c5c3085 100644
--- a/src/test/data/events.memberAdded.teamMember.json
+++ b/src/test/data/events.memberAdded.teamMember.json
@@ -2,7 +2,7 @@
"id": 1185,
"userId": 40051331,
"role": "customer",
- "isPrimary": true,
+ "isPrimary": false,
"createdAt": "2016-11-04T03:57:57.000Z",
"updatedAt": "2016-11-04T03:57:57.000Z",
"createdBy": 40152856,