Skip to content

allow update phase members with create-update phase #662

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 282 additions & 1 deletion docs/Project API.postman_collection.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"info": {
"_postman_id": "3eba12ae-a066-4d5a-bdd5-3121377e476b",
"_postman_id": "4c51e04b-42d3-4c9f-bf08-af02f51f7756",
"name": "Project API",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
Expand Down Expand Up @@ -4577,6 +4577,208 @@
{
"name": "Project Phase",
"item": [
{
"name": "Before Start",
"item": [
{
"name": "Create project type",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 201\", function () {",
" pm.response.to.have.status(201);",
" if(pm.response.status === \"Created\") {",
" const response = pm.response.json()",
" pm.environment.set(\"projectTypeId\", response.key);",
" }",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{jwt-token}}"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"key\": \"new key\",\r\n \"displayName\": \"new displayName\",\r\n \"icon\": \"http://example.com/icon4.ico\",\r\n \t\"question\": \"question 4\",\r\n \t\"info\": \"info 4\",\r\n \t\"aliases\": [\"key-41\", \"key_42\"],\r\n \t\"metadata\": {}\r\n }"
},
"url": {
"raw": "{{api-url}}/projects/metadata/projectTypes",
"host": [
"{{api-url}}"
],
"path": [
"projects",
"metadata",
"projectTypes"
]
}
},
"response": []
},
{
"name": "Create project",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 201\", function () {",
" pm.response.to.have.status(201);",
" if(pm.response.status === \"Created\") {",
" const response = pm.response.json()",
" pm.environment.set(\"projectId\", response.id);",
" }",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Authorization",
"value": "Bearer {{jwt-token}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"name\": \"test project\",\n\t\"description\": \"Hello I am a test project\",\n\t\"type\": \"{{projectTypeId}}\"\n}"
},
"url": {
"raw": "{{api-url}}/projects",
"host": [
"{{api-url}}"
],
"path": [
"projects"
]
},
"description": "Valid request body. Project should be created successfully."
},
"response": []
},
{
"name": "Create project member - 1",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 201\", function () {",
" pm.response.to.have.status(201);",
" if(pm.response.status === \"Created\") {",
" const response = pm.response.json()",
" pm.environment.set(\"phaseMemberId-1\", response.userId);",
" }",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Authorization",
"value": "Bearer {{jwt-token}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"userId\": \"40158994\",\n \"role\": \"copilot\"\n}"
},
"url": {
"raw": "{{api-url}}/projects/{{projectId}}/members",
"host": [
"{{api-url}}"
],
"path": [
"projects",
"{{projectId}}",
"members"
]
},
"description": "If the request payload is valid, than project member should be created."
},
"response": []
},
{
"name": "Create project member - 2",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 201\", function () {",
" pm.response.to.have.status(201);",
" if(pm.response.status === \"Created\") {",
" const response = pm.response.json()",
" pm.environment.set(\"phaseMemberId-2\", response.userId);",
" }",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Authorization",
"value": "Bearer {{jwt-token}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"userId\": \"40153800\",\n \"role\": \"copilot\"\n}"
},
"url": {
"raw": "{{api-url}}/projects/{{projectId}}/members",
"host": [
"{{api-url}}"
],
"path": [
"projects",
"{{projectId}}",
"members"
]
},
"description": "If the request payload is valid, than project member should be created."
},
"response": []
}
]
},
{
"name": "Create Phase",
"event": [
Expand Down Expand Up @@ -4715,6 +4917,52 @@
},
"response": []
},
{
"name": "Create Phase with members",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status code is 201\", function () {",
" pm.response.to.have.status(201);",
" pm.environment.set(\"phaseId\", pm.response.json().id);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Authorization",
"value": "Bearer {{jwt-token}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"name\": \"test project phase\",\n\t\"status\": \"active\",\n\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\"budget\": 20,\n\t\"details\": {\n\t\t\"aDetails\": \"a details\"\n\t},\n \"members\": [{{phaseMemberId-1}},{{phaseMemberId-2}}]\n}"
},
"url": {
"raw": "{{api-url}}/projects/{{projectId}}/phases",
"host": [
"{{api-url}}"
],
"path": [
"projects",
"{{projectId}}",
"phases"
]
}
},
"response": []
},
{
"name": "List Phase",
"request": {
Expand Down Expand Up @@ -5008,6 +5256,39 @@
},
"response": []
},
{
"name": "Update Phase with members",
"request": {
"method": "PATCH",
"header": [
{
"key": "Authorization",
"value": "Bearer {{jwt-token}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"name\": \"test project phase xxx\",\n\t\"status\": \"inactive\",\n\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\"budget\": 30,\n\t\"progress\": 15,\n\t\"details\": {\n\t\t\"message\": \"phase details\"\n\t},\n \"members\": [{{phaseMemberId-1}},{{phaseMemberId-2}}]\n}"
},
"url": {
"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}",
"host": [
"{{api-url}}"
],
"path": [
"projects",
"{{projectId}}",
"phases",
"{{phaseId}}"
]
}
},
"response": []
},
{
"name": "Delete Phase",
"request": {
Expand Down
4 changes: 2 additions & 2 deletions src/permissions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ module.exports = () => {
Authorizer.setPolicy('project.updatePhaseProduct', copilotAndAbove);
Authorizer.setPolicy('project.deletePhaseProduct', copilotAndAbove);

Authorizer.setPolicy('phaseMember.update', projectAdmin);
Authorizer.setPolicy('phaseMember.delete', projectAdmin);
Authorizer.setPolicy('phaseMember.update', copilotAndAbove);
Authorizer.setPolicy('phaseMember.delete', copilotAndAbove);
Authorizer.setPolicy('phaseMember.view', generalPermission(PERMISSION.READ_PROJECT_MEMBER));

Authorizer.setPolicy('milestoneTemplate.clone', projectAdmin);
Expand Down
2 changes: 1 addition & 1 deletion src/routes/phaseMembers/delete.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ describe('Delete phase member', () => {
.expect(403, done);
});

it('should return 200 for connect admin', (done) => {
it('should return 204 for connect admin', (done) => {
request(server)
.delete(`/v5/projects/${id}/phases/${phaseId}/members/${copilotUser.userId}`)
.set({
Expand Down
31 changes: 4 additions & 27 deletions src/routes/phaseMembers/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { middleware as tcMiddleware } from 'tc-core-library-js';
import models from '../../models';
import util from '../../util';
import { EVENT, RESOURCES, ROUTES } from '../../constants';
import updateService from './updateService';

/**
* API to update a project phase members.
Expand All @@ -28,10 +29,7 @@ module.exports = [
async (req, res, next) => {
const projectId = _.parseInt(req.params.projectId);
const phaseId = _.parseInt(req.params.phaseId);
const createdBy = _.parseInt(req.authUser.userId);
const updatedBy = _.parseInt(req.authUser.userId);
const newPhaseMembers = req.body.userIds;
const transaction = await models.sequelize.transaction();
try {
// chekc if project and phase exist
const phase = await models.ProjectPhase.findOne({
Expand All @@ -48,31 +46,12 @@ module.exports = [
err.status = 404;
throw (err);
}
const projectMembers = _.map(await models.ProjectMember.getActiveProjectMembers(projectId), 'userId');
const notProjectMembers = _.difference(newPhaseMembers, projectMembers);
if (notProjectMembers.length > 0) {
const err = new Error(`Members with id: ${notProjectMembers} are not members of project ${projectId}`);
err.status = 404;
throw (err);
}
const phaseMembers = await models.ProjectPhaseMember.getPhaseMembers(phaseId);
const existentPhaseMembers = _.map(phaseMembers, 'userId');
let updatedPhaseMembers = _.cloneDeep(phaseMembers);
const updatedPhase = _.cloneDeep(phase);
const membersToAdd = _.difference(newPhaseMembers, existentPhaseMembers);
const membersToRemove = _.differenceBy(existentPhaseMembers, newPhaseMembers);
if (membersToRemove.length > 0) {
await models.ProjectPhaseMember.destroy({ where: { phaseId, userId: membersToRemove }, transaction });
updatedPhaseMembers = _.filter(updatedPhaseMembers, row => !_.includes(membersToRemove, row.userId));
}
if (membersToAdd.length > 0) {
const createData = _.map(membersToAdd, userId => ({ phaseId, userId, createdBy, updatedBy }));
const result = await models.ProjectPhaseMember.bulkCreate(createData, { transaction });
updatedPhaseMembers.push(..._.map(result, item => item.toJSON()));
}
const updatedPhaseMembers = await updateService(req.authUser, projectId, phaseId, newPhaseMembers);
req.log.debug('updated phase members', JSON.stringify(newPhaseMembers, null, 2));
const updatedPhase = _.cloneDeep(phase);
// emit event
if (membersToRemove.length > 0 || membersToAdd.length > 0) {
if (_.intersectionBy(phaseMembers, updatedPhaseMembers, 'id').length !== updatedPhaseMembers.length) {
util.sendResourceToKafkaBus(
req,
EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED,
Expand All @@ -81,10 +60,8 @@ module.exports = [
_.assign(phase, { members: phaseMembers }),
ROUTES.PHASES.UPDATE);
}
await transaction.commit();
res.json(updatedPhaseMembers);
} catch (err) {
await transaction.rollback();
next(err);
}
},
Expand Down
Loading