Skip to content

Commit 4823b03

Browse files
authored
Merge pull request #556 from topcoder-platform/feature/PLAT-2032
feat: add support for phase constraints (plat-2032)
2 parents 8333a17 + 57e3a03 commit 4823b03

File tree

13 files changed

+4603
-141
lines changed

13 files changed

+4603
-141
lines changed

.circleci/config.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ install_deploysuite: &install_deploysuite
1818
cp ./../buildscript/buildenv.sh .
1919
cp ./../buildscript/awsconfiguration.sh .
2020
restore_cache_settings_for_build: &restore_cache_settings_for_build
21-
key: docker-node-modules-{{ checksum "package-lock.json" }}
21+
key: docker-node-modules-{{ checksum "yarn.lock" }}
2222

2323
save_cache_settings: &save_cache_settings
24-
key: docker-node-modules-{{ checksum "package-lock.json" }}
24+
key: docker-node-modules-{{ checksum "yarn.lock" }}
2525
paths:
2626
- node_modules
2727

@@ -72,7 +72,7 @@ workflows:
7272
branches:
7373
only:
7474
- develop
75-
- fix/task-memberId-reset
75+
- feature/PLAT-2032
7676

7777
# Production builds are exectuted only on tagged commits to the
7878
# master branch.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,4 @@ typings/
6060

6161
# next.js build output
6262
.next
63+
.npmrc

.vscode/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"editor.defaultFormatter": "standard.vscode-standard",
3+
"standard.autoFixOnSave": true
4+
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ You can find sample `.env` files inside the `/docs` directory.
102102
1. 📦 Install npm dependencies
103103

104104
```bash
105-
npm install
105+
yarn install
106106
```
107107

108108
2. ⚙ Local config

build.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ docker create --name app $APP_NAME:latest
77

88
if [ -d node_modules ]
99
then
10-
mv package-lock.json old-package-lock.json
11-
docker cp app:/$APP_NAME/package-lock.json package-lock.json
10+
mv yarn.lock old-yarn.lock
11+
docker cp app:/$APP_NAME/yarn.lock yarn.lock
1212
set +eo pipefail
13-
UPDATE_CACHE=$(cmp package-lock.json old-package-lock.json)
13+
UPDATE_CACHE=$(cmp yarn.lock old-yarn.lock)
1414
set -eo pipefail
1515
else
1616
UPDATE_CACHE=1

docker/Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Use the base image with Node.js
2-
FROM node:12.22.12-buster
2+
FROM node:14.21.2-bullseye
33

44
# Copy the current directory into the Docker image
55
COPY . /challenge-api
@@ -8,6 +8,6 @@ COPY . /challenge-api
88
WORKDIR /challenge-api
99

1010
# Install the dependencies from package.json
11-
RUN npm install
11+
RUN yarn install
1212

13-
CMD npm start
13+
CMD yarn start

mock-api/Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ FROM node:10.20-jessie
22

33
COPY . /challenge-api
44

5-
RUN (cd /challenge-api && npm install)
5+
RUN (cd /challenge-api && yarn install)
66

77
WORKDIR /challenge-api/mock-api
88

9-
RUN npm install
9+
RUN yarn install
1010

11-
CMD npm start
11+
CMD yarn start

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@
7676
]
7777
},
7878
"engines": {
79-
"node": "10.x"
79+
"node": "14.x"
8080
},
8181
"volta": {
82-
"node": "12.22.12"
82+
"node": "14.21.2"
8383
}
8484
}

src/common/helper.js

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -336,25 +336,6 @@ function partialMatch (filter, value) {
336336
}
337337
}
338338

339-
/**
340-
* Perform validation on phases.
341-
* @param {Array} phases the phases data.
342-
*/
343-
async function validatePhases (phases) {
344-
if (!phases || phases.length === 0) {
345-
return
346-
}
347-
const records = await scan('Phase')
348-
const map = new Map()
349-
_.each(records, r => {
350-
map.set(r.id, r)
351-
})
352-
const invalidPhases = _.filter(phases, p => !map.has(p.phaseId))
353-
if (invalidPhases.length > 0) {
354-
throw new errors.BadRequestError(`The following phases are invalid: ${toString(invalidPhases)}`)
355-
}
356-
}
357-
358339
/**
359340
* Download file from S3
360341
* @param {String} bucket the bucket name
@@ -1294,7 +1275,6 @@ module.exports = {
12941275
scanAll,
12951276
validateDuplicate,
12961277
partialMatch,
1297-
validatePhases,
12981278
downloadFromFileStack,
12991279
downloadFromS3,
13001280
deleteFromS3,

src/common/phase-helper.js

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
const _ = require('lodash')
2+
const uuid = require('uuid/v4')
3+
const moment = require('moment')
4+
5+
const errors = require('./errors')
6+
const helper = require('./helper')
7+
8+
class ChallengePhaseHelper {
9+
/**
10+
* Populate challenge phases.
11+
* @param {Array} phases the phases to populate
12+
* @param {Date} startDate the challenge start date
13+
* @param {String} timelineTemplateId the timeline template id
14+
*/
15+
async populatePhases (phases, startDate, timelineTemplateId) {
16+
if (_.isUndefined(timelineTemplateId)) {
17+
throw new errors.BadRequestError(`Invalid timeline template ID: ${timelineTemplateId}`)
18+
}
19+
20+
const { timelineTempate, timelineTemplateMap } = await this.getTemplateAndTemplateMap(timelineTemplateId)
21+
const { phaseDefinitionMap } = await this.getPhaseDefinitionsAndMap()
22+
23+
if (!phases || phases.length === 0) {
24+
// auto populate phases
25+
for (const p of timelineTempate) {
26+
phases.push({ ...p })
27+
}
28+
}
29+
30+
for (const p of phases) {
31+
const phaseDefinition = phaseDefinitionMap.get(p.phaseId)
32+
33+
p.id = uuid()
34+
p.name = phaseDefinition.name
35+
p.description = phaseDefinition.description
36+
37+
// set p.open based on current phase
38+
const phaseTemplate = timelineTemplateMap.get(p.phaseId)
39+
if (phaseTemplate) {
40+
if (!p.duration) {
41+
p.duration = phaseTemplate.defaultDuration
42+
}
43+
44+
if (phaseTemplate.predecessor) {
45+
const predecessor = _.find(phases, { phaseId: phaseTemplate.predecessor })
46+
if (!predecessor) {
47+
throw new errors.BadRequestError(`Predecessor ${phaseTemplate.predecessor} not found in given phases.`)
48+
}
49+
p.predecessor = phaseTemplate.predecessor
50+
}
51+
}
52+
}
53+
54+
// calculate dates
55+
if (!startDate) {
56+
return
57+
}
58+
59+
// sort phases by predecessor
60+
phases.sort((a, b) => {
61+
if (a.predecessor === b.phaseId) {
62+
return 1
63+
}
64+
if (b.predecessor === a.phaseId) {
65+
return -1
66+
}
67+
return 0
68+
})
69+
70+
let isSubmissionPhaseOpen = false
71+
72+
for (let p of phases) {
73+
const predecessor = timelineTemplateMap.get(p.predecessor)
74+
75+
if (predecessor == null) {
76+
if (p.name === 'Registration') {
77+
p.scheduledStartDate = moment(startDate).toDate()
78+
}
79+
if (p.name === 'Submission') {
80+
if (p.scheduledStartDate != null) {
81+
p.scheduledStartDate = moment(p.scheduledStartDate).toDate()
82+
} else {
83+
p.scheduledStartDate = moment(startDate).add(5, 'minutes').toDate()
84+
}
85+
}
86+
87+
if (moment(p.scheduledStartDate).isSameOrBefore(moment())) {
88+
p.actualStartDate = p.scheduledStartDate
89+
} else {
90+
delete p.actualStartDate
91+
}
92+
93+
p.scheduledEndDate = moment(p.scheduledStartDate).add(p.duration, 'seconds').toDate()
94+
if (moment(p.scheduledEndDate).isBefore(moment())) {
95+
delete p.actualEndDate
96+
} else {
97+
p.actualEndDate = p.scheduledEndDate
98+
}
99+
} else {
100+
const precedecessorPhase = _.find(phases, { phaseId: predecessor.phaseId })
101+
if (precedecessorPhase == null) {
102+
throw new errors.BadRequestError(`Predecessor ${predecessor.phaseId} not found in given phases.`)
103+
}
104+
let phaseEndDate = moment(precedecessorPhase.scheduledEndDate)
105+
if (precedecessorPhase.actualEndDate != null && moment(precedecessorPhase.actualEndDate).isAfter(phaseEndDate)) {
106+
phaseEndDate = moment(precedecessorPhase.actualEndDate)
107+
} else {
108+
phaseEndDate = moment(precedecessorPhase.scheduledEndDate)
109+
}
110+
111+
p.scheduledStartDate = phaseEndDate.toDate()
112+
p.scheduledEndDate = moment(p.scheduledStartDate).add(p.duration, 'seconds').toDate()
113+
}
114+
115+
p.isOpen = moment().isBetween(p.scheduledStartDate, p.scheduledEndDate)
116+
117+
if (p.isOpen) {
118+
if (p.name === 'Submission') {
119+
isSubmissionPhaseOpen = true
120+
}
121+
delete p.actualEndDate
122+
}
123+
124+
if (moment(p.scheduledStartDate).isAfter(moment())) {
125+
delete p.actualStartDate
126+
delete p.actualEndDate
127+
}
128+
129+
if (p.name === 'Post-Mortem' && isSubmissionPhaseOpen) {
130+
delete p.actualStartDate
131+
delete p.actualEndDate
132+
p.isOpen = false
133+
}
134+
}
135+
136+
// phases.sort((a, b) => moment(a.scheduledStartDate).isAfter(b.scheduledStartDate))
137+
}
138+
139+
async validatePhases (phases) {
140+
if (!phases || phases.length === 0) {
141+
return
142+
}
143+
const records = await helper.scan('Phase')
144+
const map = new Map()
145+
_.each(records, (r) => {
146+
map.set(r.id, r)
147+
})
148+
const invalidPhases = _.filter(phases, (p) => !map.has(p.phaseId))
149+
if (invalidPhases.length > 0) {
150+
throw new errors.BadRequestError(
151+
`The following phases are invalid: ${toString(invalidPhases)}`
152+
)
153+
}
154+
}
155+
156+
async getPhaseDefinitionsAndMap () {
157+
const records = await helper.scan('Phase')
158+
const map = new Map()
159+
_.each(records, (r) => {
160+
map.set(r.id, r)
161+
})
162+
return { phaseDefinitions: records, phaseDefinitionMap: map }
163+
}
164+
165+
async getTemplateAndTemplateMap (timelineTemplateId) {
166+
const records = await helper.getById('TimelineTemplate', timelineTemplateId)
167+
const map = new Map()
168+
_.each(records.phases, (r) => {
169+
map.set(r.phaseId, r)
170+
})
171+
172+
return {
173+
timelineTempate: records.phases,
174+
timelineTemplateMap: map
175+
}
176+
}
177+
}
178+
179+
module.exports = new ChallengePhaseHelper()

0 commit comments

Comments
 (0)