Skip to content

Issue #127 #144

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
merged 6 commits into from
Jul 31, 2018
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
10 changes: 5 additions & 5 deletions postman.json
Original file line number Diff line number Diff line change
Expand Up @@ -3802,7 +3802,7 @@
],
"body": {
"mode": "raw",
"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}"
"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}"
},
"url": {
"raw": "{{api-url}}/v4/timelines/1/milestones/1",
Expand Down Expand Up @@ -3836,7 +3836,7 @@
],
"body": {
"mode": "raw",
"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}"
"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}"
},
"url": {
"raw": "{{api-url}}/v4/timelines/1/milestones/1",
Expand Down Expand Up @@ -3870,7 +3870,7 @@
],
"body": {
"mode": "raw",
"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}"
"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}"
},
"url": {
"raw": "{{api-url}}/v4/timelines/1/milestones/1",
Expand Down Expand Up @@ -3904,7 +3904,7 @@
],
"body": {
"mode": "raw",
"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}"
"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}"
},
"url": {
"raw": "{{api-url}}/v4/timelines/1/milestones/1",
Expand Down Expand Up @@ -3938,7 +3938,7 @@
],
"body": {
"mode": "raw",
"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}"
"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}"
},
"url": {
"raw": "{{api-url}}/v4/timelines/1/milestones/1",
Expand Down
97 changes: 76 additions & 21 deletions src/routes/milestones/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import validate from 'express-validation';
import _ from 'lodash';
import moment from 'moment';
import Joi from 'joi';
import Sequelize from 'sequelize';
import { middleware as tcMiddleware } from 'tc-core-library-js';
Expand All @@ -13,6 +14,52 @@ import models from '../../models';

const permissions = tcMiddleware.permissions;

/**
* Cascades endDate/completionDate changes to all milestones with a greater order than the given one.
* @param {Object} updatedMilestone the milestone that was updated
* @returns {Promise<void>} a promise that resolves to the last found milestone. If no milestone exists with an
* order greater than the passed <b>updatedMilestone</b>, the promise will resolve to the passed
* <b>updatedMilestone</b>
*/
function updateComingMilestones(updatedMilestone) {
return models.Milestone.findAll({
where: {
timelineId: updatedMilestone.timelineId,
order: { $gt: updatedMilestone.order },
},
}).then((affectedMilestones) => {
const comingMilestones = _.sortBy(affectedMilestones, 'order');
let startDate = moment.utc(updatedMilestone.completionDate
? updatedMilestone.completionDate
: updatedMilestone.endDate).add(1, 'days').toDate();
const promises = _.map(comingMilestones, (_milestone) => {
const milestone = _milestone;

// Update the milestone startDate if different than the iterated startDate
if (!_.isEqual(milestone.startDate, startDate)) {
milestone.startDate = startDate;
milestone.updatedBy = updatedMilestone.updatedBy;
}

// Calculate the endDate, and update it if different
const endDate = moment.utc(startDate).add(milestone.duration - 1, 'days').toDate();
if (!_.isEqual(milestone.endDate, endDate)) {
milestone.endDate = endDate;
milestone.updatedBy = updatedMilestone.updatedBy;
}

// Set the next startDate value to the next day after completionDate if present or the endDate
startDate = moment.utc(milestone.completionDate
? milestone.completionDate
: milestone.endDate).add(1, 'days').toDate();
return milestone.save();
});

// Resolve promise to the last updated milestone, or to the passed in updatedMilestone
return Promise.all(promises).then(updatedMilestones => updatedMilestones.pop() || updatedMilestone);
});
}

const schema = {
params: {
timelineId: Joi.number().integer().positive().required(),
Expand All @@ -23,9 +70,9 @@ const schema = {
id: Joi.any().strip(),
name: Joi.string().max(255).optional(),
description: Joi.string().max(255),
duration: Joi.number().integer().optional(),
startDate: Joi.date().optional(),
endDate: Joi.date().allow(null),
duration: Joi.number().integer().min(1).optional(),
startDate: Joi.any().forbidden(),
endDate: Joi.any().forbidden(),
completionDate: Joi.date().allow(null),
status: Joi.string().max(45).optional(),
type: Joi.string().max(45).optional(),
Expand Down Expand Up @@ -62,24 +109,7 @@ module.exports = [
timelineId: req.params.timelineId,
});

// Validate startDate and endDate to be within the timeline startDate and endDate
let error;
if (req.body.param.startDate < req.timeline.startDate) {
error = 'Milestone startDate must not be before the timeline startDate';
} else if (req.body.param.endDate && req.timeline.endDate && req.body.param.endDate > req.timeline.endDate) {
error = 'Milestone endDate must not be after the timeline endDate';
}
if (entityToUpdate.endDate && entityToUpdate.endDate < entityToUpdate.startDate) {
error = 'Milestone endDate must not be before startDate';
}
if (entityToUpdate.completionDate && entityToUpdate.completionDate < entityToUpdate.startDate) {
error = 'Milestone endDate must not be before startDate';
}
if (error) {
const apiErr = new Error(error);
apiErr.status = 422;
return next(apiErr);
}
const timeline = req.timeline;

let original;
let updated;
Expand All @@ -95,11 +125,21 @@ module.exports = [
return Promise.reject(apiErr);
}

if (entityToUpdate.completionDate && entityToUpdate.completionDate < milestone.startDate) {
const apiErr = new Error('The milestone completionDate should be greater or equal than the startDate.');
apiErr.status = 422;
return Promise.reject(apiErr);
}

original = _.omit(milestone.toJSON(), ['deletedAt', 'deletedBy']);

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

if (entityToUpdate.duration && entityToUpdate.duration !== milestone.duration) {
entityToUpdate.endDate = moment.utc(milestone.startDate).add(entityToUpdate.duration - 1, 'days').toDate();
}

// Update
return milestone.update(entityToUpdate);
})
Expand Down Expand Up @@ -146,6 +186,21 @@ module.exports = [
},
});
});
})
.then(() => {
// Update dates of the other milestones only if the completionDate or the duration changed
if (!_.isEqual(original.completionDate, updated.completionDate) || original.duration !== updated.duration) {
return updateComingMilestones(updated)
.then((lastTimelineMilestone) => {
if (!_.isEqual(lastTimelineMilestone.endDate, timeline.endDate)) {
timeline.endDate = lastTimelineMilestone.endDate;
timeline.updatedBy = lastTimelineMilestone.updatedBy;
return timeline.save();
}
return Promise.resolve();
});
}
return Promise.resolve();
}),
)
.then(() => {
Expand Down
Loading