Skip to content

Commit b9fc2db

Browse files
committed
- Updating a milestone's completionDate/duration should update the dates of subsequent milestones.
- Fixed bug where the transaction wasn't being included into wrapped queries.
1 parent a2cced2 commit b9fc2db

File tree

2 files changed

+141
-47
lines changed

2 files changed

+141
-47
lines changed

src/routes/milestones/update.js

Lines changed: 68 additions & 22 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,37 @@ 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+
* @param {Object} transaction the wrapping transaction
21+
* @returns {Promise<void>} a promise
22+
*/
23+
async function updateComingMilestones(updatedMilestone, transaction) {
24+
const comingMilestones = _.sortBy(await models.Milestone.findAll({
25+
where: {
26+
timelineId: updatedMilestone.timelineId,
27+
order: { $gt: updatedMilestone.order },
28+
},
29+
transaction,
30+
}), 'order');
31+
let startDate = moment.utc(updatedMilestone.completionDate
32+
? updatedMilestone.completionDate
33+
: updatedMilestone.endDate).add(1, 'days').toDate();
34+
const promises = _.map(comingMilestones, (_milestone) => {
35+
const milestone = _milestone;
36+
if (milestone.startDate.getTime() !== startDate.getTime()) {
37+
milestone.startDate = startDate;
38+
milestone.endDate = moment.utc(startDate).add(milestone.duration - 1, 'days').toDate();
39+
}
40+
startDate = moment.utc(milestone.completionDate
41+
? milestone.completionDate
42+
: milestone.endDate).add(1, 'days').toDate();
43+
return milestone.save({ transaction });
44+
});
45+
await Promise.all(promises);
46+
}
47+
1648
const schema = {
1749
params: {
1850
timelineId: Joi.number().integer().positive().required(),
@@ -23,7 +55,7 @@ const schema = {
2355
id: Joi.any().strip(),
2456
name: Joi.string().max(255).optional(),
2557
description: Joi.string().max(255),
26-
duration: Joi.number().integer().optional(),
58+
duration: Joi.number().integer().min(1).optional(),
2759
startDate: Joi.date().optional(),
2860
endDate: Joi.date().allow(null),
2961
completionDate: Joi.date().allow(null),
@@ -62,29 +94,10 @@ module.exports = [
6294
timelineId: req.params.timelineId,
6395
});
6496

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-
}
83-
8497
let original;
8598
let updated;
8699

87-
return models.sequelize.transaction(() =>
100+
return models.sequelize.transaction(transaction =>
88101
// Find the milestone
89102
models.Milestone.findOne({ where })
90103
.then((milestone) => {
@@ -94,14 +107,34 @@ module.exports = [
94107
apiErr.status = 404;
95108
return Promise.reject(apiErr);
96109
}
110+
// if any of these keys was provided and is different from what's in the database, error
111+
if (['startDate', 'endDate']
112+
.some(key => entityToUpdate[key] && (
113+
!milestone[key] ||
114+
(milestone[key] && entityToUpdate[key].getTime() !== milestone[key].getTime())
115+
))) {
116+
const apiErr = new Error('Updating a milestone startDate or endDate is not allowed');
117+
apiErr.status = 422;
118+
return Promise.reject(apiErr);
119+
}
120+
121+
if (entityToUpdate.completionDate && entityToUpdate.completionDate < milestone.startDate) {
122+
const apiErr = new Error('The milestone completionDate should be greater or equal than the startDate.');
123+
apiErr.status = 422;
124+
return Promise.reject(apiErr);
125+
}
97126

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

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

132+
if (entityToUpdate.duration && entityToUpdate.duration !== milestone.duration) {
133+
entityToUpdate.endDate = moment.utc(milestone.startDate).add(entityToUpdate.duration - 1, 'days').toDate();
134+
}
135+
103136
// Update
104-
return milestone.update(entityToUpdate);
137+
return milestone.update(entityToUpdate, { transaction });
105138
})
106139
.then((updatedMilestone) => {
107140
// Omit deletedAt, deletedBy
@@ -118,6 +151,7 @@ module.exports = [
118151
id: { $ne: updated.id },
119152
order: updated.order,
120153
},
154+
transaction,
121155
})
122156
.then((count) => {
123157
if (count === 0) {
@@ -133,6 +167,7 @@ module.exports = [
133167
id: { $ne: updated.id },
134168
order: { $between: [original.order + 1, updated.order] },
135169
},
170+
transaction,
136171
});
137172
}
138173

@@ -144,8 +179,19 @@ module.exports = [
144179
id: { $ne: updated.id },
145180
order: { $between: [updated.order, original.order - 1] },
146181
},
182+
transaction,
147183
});
148184
});
185+
})
186+
.then(() => {
187+
// Update dates of the other milestones only if the completionDate nor the duration changed
188+
if (((!original.completionDate && !updated.completionDate) ||
189+
(original.completionDate && updated.completionDate &&
190+
original.completionDate.getTime() === updated.completionDate.getTime())) &&
191+
original.duration === updated.duration) {
192+
return Promise.resolve();
193+
}
194+
return updateComingMilestones(updated, transaction);
149195
}),
150196
)
151197
.then(() => {

src/routes/milestones/update.spec.js

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,6 @@ describe('UPDATE Milestone', () => {
256256
param: {
257257
name: 'Milestone 1-updated',
258258
duration: 3,
259-
startDate: '2018-05-14T00:00:00.000Z',
260-
endDate: '2018-05-15T00:00:00.000Z',
261259
completionDate: '2018-05-16T00:00:00.000Z',
262260
description: 'description-updated',
263261
status: 'closed',
@@ -482,28 +480,10 @@ describe('UPDATE Milestone', () => {
482480
.expect(422, done);
483481
});
484482

485-
it('should return 422 if startDate is after completionDate', (done) => {
483+
it('should return 422 if startDate is different than the original startDate', (done) => {
486484
const invalidBody = {
487485
param: _.assign({}, body.param, {
488-
startDate: '2018-05-29T00:00:00.000Z',
489-
completionDate: '2018-05-28T00:00:00.000Z',
490-
}),
491-
};
492-
493-
request(server)
494-
.patch('/v4/timelines/1/milestones/1')
495-
.set({
496-
Authorization: `Bearer ${testUtil.jwts.admin}`,
497-
})
498-
.send(invalidBody)
499-
.expect('Content-Type', /json/)
500-
.expect(422, done);
501-
});
502-
503-
it('should return 422 if startDate is before timeline startDate', (done) => {
504-
const invalidBody = {
505-
param: _.assign({}, body.param, {
506-
startDate: '2018-05-01T00:00:00.000Z',
486+
startDate: '2018-07-01T00:00:00.000Z',
507487
}),
508488
};
509489

@@ -517,7 +497,7 @@ describe('UPDATE Milestone', () => {
517497
.expect(422, done);
518498
});
519499

520-
it('should return 422 if endDate is after timeline endDate', (done) => {
500+
it('should return 422 if endDate is different than the original endDate', (done) => {
521501
const invalidBody = {
522502
param: _.assign({}, body.param, {
523503
endDate: '2018-07-01T00:00:00.000Z',
@@ -548,8 +528,6 @@ describe('UPDATE Milestone', () => {
548528
resJson.name.should.be.eql(body.param.name);
549529
resJson.description.should.be.eql(body.param.description);
550530
resJson.duration.should.be.eql(body.param.duration);
551-
resJson.startDate.should.be.eql(body.param.startDate);
552-
resJson.endDate.should.be.eql(body.param.endDate);
553531
resJson.completionDate.should.be.eql(body.param.completionDate);
554532
resJson.status.should.be.eql(body.param.status);
555533
resJson.type.should.be.eql(body.param.type);
@@ -898,6 +876,76 @@ describe('UPDATE Milestone', () => {
898876
});
899877
});
900878

879+
it('should return 200 for admin - changing completionDate will cascade changes to coming ' +
880+
// eslint-disable-next-line func-names
881+
'milestones', function (done) {
882+
this.timeout(10000);
883+
884+
request(server)
885+
.patch('/v4/timelines/1/milestones/2')
886+
.set({
887+
Authorization: `Bearer ${testUtil.jwts.admin}`,
888+
})
889+
.send({ param: _.assign({}, body.param, {
890+
completionDate: '2018-05-18T00:00:00.000Z', order: undefined, duration: undefined,
891+
}) })
892+
.expect(200)
893+
.end(() => {
894+
// Milestone 3: startDate: '2018-05-14T00:00:00.000Z' to '2018-05-19T00:00:00.000Z'
895+
// endDate: null to '2018-05-21T00:00:00.000Z'
896+
// Milestone 4: startDate: '2018-05-14T00:00:00.000Z' to '2018-05-22T00:00:00.000Z'
897+
// endDate: null to '2018-05-24T00:00:00.000Z'
898+
setTimeout(() => {
899+
models.Milestone.findById(3)
900+
.then((milestone) => {
901+
milestone.startDate.should.be.eql(new Date('2018-05-19T00:00:00.000Z'));
902+
milestone.endDate.should.be.eql(new Date('2018-05-21T00:00:00.000Z'));
903+
return models.Milestone.findById(4);
904+
})
905+
.then((milestone) => {
906+
milestone.startDate.should.be.eql(new Date('2018-05-22T00:00:00.000Z'));
907+
milestone.endDate.should.be.eql(new Date('2018-05-24T00:00:00.000Z'));
908+
done();
909+
})
910+
.catch(done);
911+
}, 3000);
912+
});
913+
});
914+
915+
it('should return 200 for admin - changing duration will cascade changes to coming ' +
916+
// eslint-disable-next-line func-names
917+
'milestones', function (done) {
918+
this.timeout(10000);
919+
920+
request(server)
921+
.patch('/v4/timelines/1/milestones/2')
922+
.set({
923+
Authorization: `Bearer ${testUtil.jwts.admin}`,
924+
})
925+
.send({ param: _.assign({}, body.param, { duration: 5, order: undefined, completionDate: undefined }) })
926+
.expect(200)
927+
.end(() => {
928+
// Milestone 3: startDate: '2018-05-14T00:00:00.000Z' to '2018-05-19T00:00:00.000Z'
929+
// endDate: null to '2018-05-21T00:00:00.000Z'
930+
// Milestone 4: startDate: '2018-05-14T00:00:00.000Z' to '2018-05-22T00:00:00.000Z'
931+
// endDate: null to '2018-05-24T00:00:00.000Z'
932+
setTimeout(() => {
933+
models.Milestone.findById(3)
934+
.then((milestone) => {
935+
milestone.startDate.should.be.eql(new Date('2018-05-19T00:00:00.000Z'));
936+
milestone.endDate.should.be.eql(new Date('2018-05-21T00:00:00.000Z'));
937+
return models.Milestone.findById(4);
938+
})
939+
.then((milestone) => {
940+
milestone.startDate.should.be.eql(new Date('2018-05-22T00:00:00.000Z'));
941+
milestone.endDate.should.be.eql(new Date('2018-05-24T00:00:00.000Z'));
942+
done();
943+
})
944+
.catch(done);
945+
}, 3000);
946+
});
947+
});
948+
901949
it('should return 200 for connect admin', (done) => {
902950
request(server)
903951
.patch('/v4/timelines/1/milestones/1')

0 commit comments

Comments
 (0)