Skip to content

Commit 027039b

Browse files
author
vikasrohit
authored
Merge pull request #279 from topcoder-platform/feature/reviewed-projects-for-copilots
Feature/reviewed projects for copilots
2 parents d111223 + c917f1d commit 027039b

File tree

13 files changed

+293
-134
lines changed

13 files changed

+293
-134
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ Microservice to manage CRUD operations for all things Projects.
7676
*NOTE: This will first clear all the indices and than recreate them. So use with caution.*
7777

7878
* Run
79+
80+
**NOTE** If you use `config/m2m.local.js` config, you should set M2M environment variables before running the next command.
7981
```bash
8082
npm run start:dev
8183
```
@@ -127,6 +129,9 @@ eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI
127129
It's been signed with the secret 'secret'. This secret should match your entry in config/local.js. You can generate your own token using https://jwt.io
128130

129131
### Local Deployment
132+
133+
**NOTE: This part of README may contain inconsistencies and requires update. Don't follow it unless you know how to properly make configuration for these steps. It's not needed for regular development process.**
134+
130135
Build image:
131136
`docker build -t tc_projects_services .`
132137
Run image:

config/m2m.local.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ if (process.env.NODE_ENV === 'test') {
55
config = require('./test.json');
66
} else {
77
config = {
8-
identityServiceEndpoint: "https://api.topcoder-dev.com/",
8+
identityServiceEndpoint: "https://api.topcoder-dev.com/v3/",
99
authSecret: 'secret',
1010
authDomain: 'topcoder-dev.com',
1111
logLevel: 'debug',

local/seed/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ async function seed() {
1010
await seedProjects(targetUrl, token);
1111
}
1212

13-
seed();
13+
seed().then(() => process.exit());

local/seed/projects.json

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,5 +174,91 @@
174174
"status": "cancelled",
175175
"cancelReason": "Test cancel"
176176
}
177+
},
178+
{
179+
"param": {
180+
"name": "Reviewed project with copilot invited",
181+
"details": {
182+
"utm": {
183+
"code": ""
184+
},
185+
"appDefinition": {
186+
"primaryTarget": "phone",
187+
"goal": {
188+
"value": "Nothing"
189+
},
190+
"users": {
191+
"value": "No one"
192+
},
193+
"notes": ""
194+
},
195+
"hideDiscussions": true
196+
},
197+
"description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message",
198+
"templateId": 3,
199+
"type": "website",
200+
"status": "reviewed",
201+
"invites": [{
202+
"param": {
203+
"userIds": [40152855],
204+
"role": "copilot"
205+
}}]
206+
}
207+
},
208+
{
209+
"param": {
210+
"name": "Reviewed project with copilot as a member with copilot role",
211+
"details": {
212+
"utm": {
213+
"code": ""
214+
},
215+
"appDefinition": {
216+
"primaryTarget": "phone",
217+
"goal": {
218+
"value": "Nothing"
219+
},
220+
"users": {
221+
"value": "No one"
222+
},
223+
"notes": ""
224+
},
225+
"hideDiscussions": true
226+
},
227+
"description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message",
228+
"templateId": 3,
229+
"type": "website",
230+
"status": "reviewed",
231+
"invites": [{
232+
"param": {
233+
"userIds": [40152855],
234+
"role": "copilot"
235+
}}],
236+
"acceptInvitation": true
237+
}
238+
},
239+
{
240+
"param": {
241+
"name": "Reviewed project when copilot is not a member and not invited",
242+
"details": {
243+
"utm": {
244+
"code": ""
245+
},
246+
"appDefinition": {
247+
"primaryTarget": "phone",
248+
"goal": {
249+
"value": "Nothing"
250+
},
251+
"users": {
252+
"value": "No one"
253+
},
254+
"notes": ""
255+
},
256+
"hideDiscussions": true
257+
},
258+
"description": "Hello this is a sample description... This requires at least 160 characters. I'm trying to satisfy this condition. But I could n't if I don't type this unnecessary message",
259+
"templateId": 3,
260+
"type": "website",
261+
"status": "reviewed"
262+
}
177263
}
178264
]

local/seed/seedProjects.js

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,88 @@
1+
import util from '../../src/tests/util';
2+
13
const axios = require('axios');
24
const Promise = require('bluebird');
35
const _ = require('lodash');
4-
56
const projects = require('./projects.json');
67

8+
// we make delay after requests which has to be indexed in ES asynchronous
9+
const ES_INDEX_DELAY = 3000;
10+
711
/**
812
* Create projects and update their statuses.
913
*/
1014
module.exports = (targetUrl, token) => {
1115
let projectPromises;
1216

1317
const projectsUrl = `${targetUrl}projects`;
14-
const headers = {
18+
const adminHeaders = {
1519
'Content-Type': 'application/json',
1620
Authorization: `Bearer ${token}`,
1721
};
1822

23+
const connectAdminHeaders = {
24+
'Content-Type': 'application/json',
25+
Authorization: `Bearer ${util.jwts.connectAdmin}`,
26+
};
27+
1928
console.log('Creating projects');
2029
projectPromises = projects.map((project, i) => {
2130
const status = _.get(project, 'param.status');
2231
const cancelReason = _.get(project, 'param.cancelReason');
32+
const invites = _.cloneDeep(_.get(project, 'param.invites'));
33+
const acceptInvitation = _.get(project, 'param.acceptInvitation');
34+
2335
delete project.param.status;
2436
delete project.param.cancelReason;
37+
delete project.param.invites;
38+
delete project.param.acceptInvitation;
2539

2640
return axios
27-
.post(projectsUrl, project, { headers })
41+
.post(projectsUrl, project, { headers: adminHeaders })
2842
.catch((err) => {
2943
console.log(`Failed to create project ${i}: ${err.message}`);
3044
})
31-
.then((response) => {
45+
.then(async (response) => {
3246
const projectId = _.get(response, 'data.result.content.id');
3347

48+
// updating status
49+
if (status !== _.get(response, 'data.result.content.status')) {
50+
console.log(`Project #${projectId}: Wait a bit to give time ES to index before updating status...`);
51+
await Promise.delay(ES_INDEX_DELAY);
52+
await updateProjectStatus(projectId, { status, cancelReason }, targetUrl, adminHeaders).catch((ex) => {
53+
console.error(`Project #${projectId}: Failed to update project status: ${ex.message}`);
54+
});
55+
}
56+
57+
// creating invitations
58+
if (Array.isArray(invites)) {
59+
let promises = []
60+
invites.forEach(invite => {
61+
promises.push(createProjectMemberInvite(projectId, invite, targetUrl, connectAdminHeaders))
62+
})
63+
64+
// accepting invitations
65+
console.log(`Project #${projectId}: Wait a bit to give time ES to index before creating invitation...`);
66+
await Promise.delay(ES_INDEX_DELAY);
67+
const responses = await Promise.all(promises)
68+
if (acceptInvitation) {
69+
let acceptInvitationPromises = []
70+
responses.forEach(response => {
71+
const userId = _.get(response, 'data.result.content.success[0].userId')
72+
acceptInvitationPromises.push(updateProjectMemberInvite(projectId, {
73+
param: {
74+
userId,
75+
status: 'accepted'
76+
}
77+
}, targetUrl, connectAdminHeaders))
78+
})
79+
80+
console.log(`Project #${projectId}: Wait a bit to give time ES to index before accepting invitation...`);
81+
await Promise.delay(ES_INDEX_DELAY);
82+
await Promise.all(acceptInvitationPromises)
83+
}
84+
}
85+
3486
return {
3587
projectId,
3688
status,
@@ -40,16 +92,6 @@ module.exports = (targetUrl, token) => {
4092
});
4193

4294
return Promise.all(projectPromises)
43-
.then((createdProjects) => {
44-
console.log('Updating statuses');
45-
return Promise.all(
46-
createdProjects.map(({ projectId, status, cancelReason }) =>
47-
updateProjectStatus(projectId, { status, cancelReason }, targetUrl, headers).catch((ex) => {
48-
console.log(`Failed to update project status of project with id ${projectId}: ${ex.message}`);
49-
}),
50-
),
51-
);
52-
})
5395
.then(() => console.log('Done project seed.'))
5496
.catch(ex => console.error(ex));
5597
};
@@ -72,3 +114,24 @@ function updateProjectStatus(project, updateParams, targetUrl, headers) {
72114
},
73115
);
74116
}
117+
118+
function createProjectMemberInvite(projectId, params, targetUrl, headers) {
119+
const projectMemberInviteUrl = `${targetUrl}projects/${projectId}/members/invite`;
120+
121+
return axios
122+
.post(projectMemberInviteUrl, params, { headers })
123+
.catch((err) => {
124+
console.log(`Failed to create project member invites ${projectId}: ${err.message}`);
125+
})
126+
}
127+
128+
function updateProjectMemberInvite(projectId, params, targetUrl, headers) {
129+
const updateProjectMemberInviteUrl = `${targetUrl}projects/${projectId}/members/invite`;
130+
131+
return axios
132+
.put(updateProjectMemberInviteUrl, params, { headers })
133+
.catch((err) => {
134+
console.log(`Failed to update project member invites ${projectId}: ${err.message}`);
135+
})
136+
}
137+

src/models/project.js

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable valid-jsdoc */
22

33
import _ from 'lodash';
4-
import { PROJECT_STATUS, PROJECT_MEMBER_ROLE } from '../constants';
4+
import { PROJECT_STATUS } from '../constants';
55

66
module.exports = function defineProject(sequelize, DataTypes) {
77
const Project = sequelize.define('Project', {
@@ -61,28 +61,6 @@ module.exports = function defineProject(sequelize, DataTypes) {
6161
{ fields: ['directProjectId'] },
6262
],
6363
classMethods: {
64-
/*
65-
* @Co-pilots should be able to view projects any of the following conditions are met:
66-
* a. they are registered active project members on the project
67-
* b. any project that is in 'reviewed' state AND does not yet have a co-pilot assigned
68-
* @param userId the id of user
69-
*/
70-
getProjectIdsForCopilot(userId) {
71-
return this.findAll({
72-
where: {
73-
$or: [
74-
['EXISTS(SELECT * FROM "project_members" WHERE "deletedAt" ' +
75-
'IS NULL AND "projectId" = "Project".id AND "userId" = ? )', userId],
76-
['"Project".status=? AND NOT EXISTS(SELECT * FROM "project_members" WHERE ' +
77-
' "deletedAt" IS NULL AND "projectId" = "Project".id AND "role" = ? )',
78-
PROJECT_STATUS.REVIEWED, PROJECT_MEMBER_ROLE.COPILOT],
79-
],
80-
},
81-
attributes: ['id'],
82-
raw: true,
83-
})
84-
.then(res => _.map(res, 'id'));
85-
},
8664
/**
8765
* Get direct project id
8866
* @param id the id of project

src/permissions/project.view.js

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import _ from 'lodash';
33
import util from '../util';
44
import models from '../models';
5-
import { USER_ROLE, PROJECT_STATUS, PROJECT_MEMBER_ROLE, MANAGER_ROLES } from '../constants';
5+
import { MANAGER_ROLES } from '../constants';
66

77
/**
88
* Super admin, Topcoder Managers are allowed to view any projects
@@ -24,48 +24,11 @@ module.exports = freq => new Promise((resolve, reject) => {
2424
|| util.hasRoles(req, MANAGER_ROLES)
2525
|| !_.isUndefined(_.find(members, m => m.userId === currentUserId));
2626

27-
// if user is co-pilot and the project doesn't have any copilots then
28-
// user can access the project
29-
if (!hasAccess && util.hasRole(req, USER_ROLE.COPILOT)) {
30-
return models.Project.getProjectIdsForCopilot(currentUserId)
31-
.then((ids) => {
32-
req.context.accessibleProjectIds = ids;
33-
return Promise.resolve(_.indexOf(ids, projectId) > -1);
34-
});
35-
}
3627
return Promise.resolve(hasAccess);
3728
})
3829
.then((hasAccess) => {
3930
if (!hasAccess) {
40-
let errorMessage = 'You do not have permissions to perform this action';
41-
// customize error message for copilots
42-
if (util.hasRole(freq, USER_ROLE.COPILOT)) {
43-
if (_.findIndex(freq.context.currentProjectMembers, m => m.role === PROJECT_MEMBER_ROLE.COPILOT) >= 0) {
44-
errorMessage = 'Copilot: Project is already claimed by another copilot';
45-
return Promise.resolve(errorMessage);
46-
}
47-
return models.Project
48-
.find({
49-
where: { id: projectId },
50-
attributes: ['status'],
51-
raw: true,
52-
})
53-
.then((project) => {
54-
if (!project || [PROJECT_STATUS.DRAFT, PROJECT_STATUS.IN_REVIEW].indexOf(project.status) >= 0) {
55-
errorMessage = 'Copilot: Project is not yet available to copilots';
56-
} else {
57-
// project status is 'active' or higher so it's not available to copilots
58-
errorMessage = 'Copilot: Project has already started';
59-
}
60-
return Promise.resolve(errorMessage);
61-
});
62-
}
63-
// user is not an admin nor is a registered project member
64-
return Promise.resolve(errorMessage);
65-
}
66-
return Promise.resolve(null);
67-
}).then((errorMessage) => {
68-
if (errorMessage) {
31+
const errorMessage = 'You do not have permissions to perform this action';
6932
// user is not an admin nor is a registered project member
7033
return reject(new Error(errorMessage));
7134
}

src/routes/projectMemberInvites/create.spec.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,27 @@ describe('Project Member Invite create', () => {
6565
lastActivityUserId: '1',
6666
}).then((p2) => {
6767
project2 = p2;
68-
models.ProjectMemberInvite.create({
69-
projectId: project1.id,
70-
userId: 40051335,
71-
email: null,
72-
role: PROJECT_MEMBER_ROLE.MANAGER,
73-
status: INVITE_STATUS.PENDING,
68+
models.ProjectMember.create({
69+
userId: 40051332,
70+
projectId: project2.id,
71+
role: 'copilot',
72+
isPrimary: true,
7473
createdBy: 1,
7574
updatedBy: 1,
76-
createdAt: '2016-06-30 00:33:07+00',
77-
updatedAt: '2016-06-30 00:33:07+00',
7875
}).then(() => {
79-
done();
76+
models.ProjectMemberInvite.create({
77+
projectId: project1.id,
78+
userId: 40051335,
79+
email: null,
80+
role: PROJECT_MEMBER_ROLE.MANAGER,
81+
status: INVITE_STATUS.PENDING,
82+
createdBy: 1,
83+
updatedBy: 1,
84+
createdAt: '2016-06-30 00:33:07+00',
85+
updatedAt: '2016-06-30 00:33:07+00',
86+
}).then(() => {
87+
done();
88+
});
8089
});
8190
}));
8291
});

0 commit comments

Comments
 (0)