Skip to content

Commit 7d7e38e

Browse files
author
vikasrohit
authored
Merge pull request #144 from topcoder-platform/issue/127
Issue #127
2 parents 001ef29 + 377b070 commit 7d7e38e

File tree

4 files changed

+336
-144
lines changed

4 files changed

+336
-144
lines changed

postman.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3802,7 +3802,7 @@
38023802
],
38033803
"body": {
38043804
"mode": "raw",
3805-
"raw": "{\r\n \"param\":{\r\n \"name\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"startDate\": \"2018-05-04T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-06T00:00:00.000Z\",\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 1,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}"
3805+
"raw": "{\r\n \"param\":{\r\n \"name\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 1,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}"
38063806
},
38073807
"url": {
38083808
"raw": "{{api-url}}/v4/timelines/1/milestones/1",
@@ -3836,7 +3836,7 @@
38363836
],
38373837
"body": {
38383838
"mode": "raw",
3839-
"raw": "{\r\n \"param\":{\r\n \"name\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"startDate\": \"2018-05-04T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-06T00:00:00.000Z\",\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 2,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}"
3839+
"raw": "{\r\n \"param\":{\r\n \"name\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 2,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}"
38403840
},
38413841
"url": {
38423842
"raw": "{{api-url}}/v4/timelines/1/milestones/1",
@@ -3870,7 +3870,7 @@
38703870
],
38713871
"body": {
38723872
"mode": "raw",
3873-
"raw": "{\r\n \"param\":{\r\n \"name\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"startDate\": \"2018-05-04T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-06T00:00:00.000Z\",\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 1,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}"
3873+
"raw": "{\r\n \"param\":{\r\n \"name\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 1,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}"
38743874
},
38753875
"url": {
38763876
"raw": "{{api-url}}/v4/timelines/1/milestones/1",
@@ -3904,7 +3904,7 @@
39043904
],
39053905
"body": {
39063906
"mode": "raw",
3907-
"raw": "{\r\n \"param\":{\r\n \"name\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"startDate\": \"2018-05-04T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-06T00:00:00.000Z\",\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 3,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}"
3907+
"raw": "{\r\n \"param\":{\r\n \"name\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 3,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}"
39083908
},
39093909
"url": {
39103910
"raw": "{{api-url}}/v4/timelines/1/milestones/1",
@@ -3938,7 +3938,7 @@
39383938
],
39393939
"body": {
39403940
"mode": "raw",
3941-
"raw": "{\r\n \"param\":{\r\n \"name\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"startDate\": \"2018-05-04T00:00:00.000Z\",\r\n \"endDate\": \"2018-05-06T00:00:00.000Z\",\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 1,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}"
3941+
"raw": "{\r\n \"param\":{\r\n \"name\": \"milestone 1-updated\",\r\n \"description\": \"description-updated\",\r\n \"duration\": 3,\r\n \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n \"status\": \"closed\",\r\n \"type\": \"type2\",\r\n \"details\": {\r\n \"detail1\": {\r\n \"subDetail1C\": 3\r\n },\r\n \"detail2\": [\r\n 4\r\n ]\r\n },\r\n \"order\": 1,\r\n \"plannedText\": \"plannedText 1-updated\",\r\n \"activeText\": \"activeText 1-updated\",\r\n \"completedText\": \"completedText 1-updated\",\r\n \"blockedText\": \"blockedText 1-updated\"\r\n }\r\n}"
39423942
},
39433943
"url": {
39443944
"raw": "{{api-url}}/v4/timelines/1/milestones/1",

src/routes/milestones/update.js

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
import validate from 'express-validation';
55
import _ from 'lodash';
6+
import moment from 'moment';
67
import Joi from 'joi';
78
import Sequelize from 'sequelize';
89
import { middleware as tcMiddleware } from 'tc-core-library-js';
@@ -13,6 +14,52 @@ import models from '../../models';
1314

1415
const permissions = tcMiddleware.permissions;
1516

17+
/**
18+
* Cascades endDate/completionDate changes to all milestones with a greater order than the given one.
19+
* @param {Object} updatedMilestone the milestone that was updated
20+
* @returns {Promise<void>} a promise that resolves to the last found milestone. If no milestone exists with an
21+
* order greater than the passed <b>updatedMilestone</b>, the promise will resolve to the passed
22+
* <b>updatedMilestone</b>
23+
*/
24+
function updateComingMilestones(updatedMilestone) {
25+
return models.Milestone.findAll({
26+
where: {
27+
timelineId: updatedMilestone.timelineId,
28+
order: { $gt: updatedMilestone.order },
29+
},
30+
}).then((affectedMilestones) => {
31+
const comingMilestones = _.sortBy(affectedMilestones, 'order');
32+
let startDate = moment.utc(updatedMilestone.completionDate
33+
? updatedMilestone.completionDate
34+
: updatedMilestone.endDate).add(1, 'days').toDate();
35+
const promises = _.map(comingMilestones, (_milestone) => {
36+
const milestone = _milestone;
37+
38+
// Update the milestone startDate if different than the iterated startDate
39+
if (!_.isEqual(milestone.startDate, startDate)) {
40+
milestone.startDate = startDate;
41+
milestone.updatedBy = updatedMilestone.updatedBy;
42+
}
43+
44+
// Calculate the endDate, and update it if different
45+
const endDate = moment.utc(startDate).add(milestone.duration - 1, 'days').toDate();
46+
if (!_.isEqual(milestone.endDate, endDate)) {
47+
milestone.endDate = endDate;
48+
milestone.updatedBy = updatedMilestone.updatedBy;
49+
}
50+
51+
// Set the next startDate value to the next day after completionDate if present or the endDate
52+
startDate = moment.utc(milestone.completionDate
53+
? milestone.completionDate
54+
: milestone.endDate).add(1, 'days').toDate();
55+
return milestone.save();
56+
});
57+
58+
// Resolve promise to the last updated milestone, or to the passed in updatedMilestone
59+
return Promise.all(promises).then(updatedMilestones => updatedMilestones.pop() || updatedMilestone);
60+
});
61+
}
62+
1663
const schema = {
1764
params: {
1865
timelineId: Joi.number().integer().positive().required(),
@@ -23,9 +70,9 @@ const schema = {
2370
id: Joi.any().strip(),
2471
name: Joi.string().max(255).optional(),
2572
description: Joi.string().max(255),
26-
duration: Joi.number().integer().optional(),
27-
startDate: Joi.date().optional(),
28-
endDate: Joi.date().allow(null),
73+
duration: Joi.number().integer().min(1).optional(),
74+
startDate: Joi.any().forbidden(),
75+
endDate: Joi.any().forbidden(),
2976
completionDate: Joi.date().allow(null),
3077
status: Joi.string().max(45).optional(),
3178
type: Joi.string().max(45).optional(),
@@ -62,24 +109,7 @@ module.exports = [
62109
timelineId: req.params.timelineId,
63110
});
64111

65-
// Validate startDate and endDate to be within the timeline startDate and endDate
66-
let error;
67-
if (req.body.param.startDate < req.timeline.startDate) {
68-
error = 'Milestone startDate must not be before the timeline startDate';
69-
} else if (req.body.param.endDate && req.timeline.endDate && req.body.param.endDate > req.timeline.endDate) {
70-
error = 'Milestone endDate must not be after the timeline endDate';
71-
}
72-
if (entityToUpdate.endDate && entityToUpdate.endDate < entityToUpdate.startDate) {
73-
error = 'Milestone endDate must not be before startDate';
74-
}
75-
if (entityToUpdate.completionDate && entityToUpdate.completionDate < entityToUpdate.startDate) {
76-
error = 'Milestone endDate must not be before startDate';
77-
}
78-
if (error) {
79-
const apiErr = new Error(error);
80-
apiErr.status = 422;
81-
return next(apiErr);
82-
}
112+
const timeline = req.timeline;
83113

84114
let original;
85115
let updated;
@@ -95,11 +125,21 @@ module.exports = [
95125
return Promise.reject(apiErr);
96126
}
97127

128+
if (entityToUpdate.completionDate && entityToUpdate.completionDate < milestone.startDate) {
129+
const apiErr = new Error('The milestone completionDate should be greater or equal than the startDate.');
130+
apiErr.status = 422;
131+
return Promise.reject(apiErr);
132+
}
133+
98134
original = _.omit(milestone.toJSON(), ['deletedAt', 'deletedBy']);
99135

100136
// Merge JSON fields
101137
entityToUpdate.details = util.mergeJsonObjects(milestone.details, entityToUpdate.details);
102138

139+
if (entityToUpdate.duration && entityToUpdate.duration !== milestone.duration) {
140+
entityToUpdate.endDate = moment.utc(milestone.startDate).add(entityToUpdate.duration - 1, 'days').toDate();
141+
}
142+
103143
// Update
104144
return milestone.update(entityToUpdate);
105145
})
@@ -146,6 +186,21 @@ module.exports = [
146186
},
147187
});
148188
});
189+
})
190+
.then(() => {
191+
// Update dates of the other milestones only if the completionDate or the duration changed
192+
if (!_.isEqual(original.completionDate, updated.completionDate) || original.duration !== updated.duration) {
193+
return updateComingMilestones(updated)
194+
.then((lastTimelineMilestone) => {
195+
if (!_.isEqual(lastTimelineMilestone.endDate, timeline.endDate)) {
196+
timeline.endDate = lastTimelineMilestone.endDate;
197+
timeline.updatedBy = lastTimelineMilestone.updatedBy;
198+
return timeline.save();
199+
}
200+
return Promise.resolve();
201+
});
202+
}
203+
return Promise.resolve();
149204
}),
150205
)
151206
.then(() => {

0 commit comments

Comments
 (0)