Skip to content

feat: add support for phase constraints (plat-2032) #556

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 18 commits into from
Jan 31, 2023
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
6 changes: 3 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ install_deploysuite: &install_deploysuite
cp ./../buildscript/buildenv.sh .
cp ./../buildscript/awsconfiguration.sh .
restore_cache_settings_for_build: &restore_cache_settings_for_build
key: docker-node-modules-{{ checksum "package-lock.json" }}
key: docker-node-modules-{{ checksum "yarn.lock" }}

save_cache_settings: &save_cache_settings
key: docker-node-modules-{{ checksum "package-lock.json" }}
key: docker-node-modules-{{ checksum "yarn.lock" }}
paths:
- node_modules

Expand Down Expand Up @@ -72,7 +72,7 @@ workflows:
branches:
only:
- develop
- fix/task-memberId-reset
- feature/PLAT-2032

# Production builds are exectuted only on tagged commits to the
# master branch.
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,4 @@ typings/

# next.js build output
.next
.npmrc
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"editor.defaultFormatter": "standard.vscode-standard",
"standard.autoFixOnSave": true
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ You can find sample `.env` files inside the `/docs` directory.
1. 📦 Install npm dependencies

```bash
npm install
yarn install
```

2. ⚙ Local config
Expand Down
6 changes: 3 additions & 3 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ docker create --name app $APP_NAME:latest

if [ -d node_modules ]
then
mv package-lock.json old-package-lock.json
docker cp app:/$APP_NAME/package-lock.json package-lock.json
mv yarn.lock old-yarn.lock
docker cp app:/$APP_NAME/yarn.lock yarn.lock
set +eo pipefail
UPDATE_CACHE=$(cmp package-lock.json old-package-lock.json)
UPDATE_CACHE=$(cmp yarn.lock old-yarn.lock)
set -eo pipefail
else
UPDATE_CACHE=1
Expand Down
6 changes: 3 additions & 3 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Use the base image with Node.js
FROM node:12.22.12-buster
FROM node:14.21.2-bullseye

# Copy the current directory into the Docker image
COPY . /challenge-api
Expand All @@ -8,6 +8,6 @@ COPY . /challenge-api
WORKDIR /challenge-api

# Install the dependencies from package.json
RUN npm install
RUN yarn install

CMD npm start
CMD yarn start
6 changes: 3 additions & 3 deletions mock-api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ FROM node:10.20-jessie

COPY . /challenge-api

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

WORKDIR /challenge-api/mock-api

RUN npm install
RUN yarn install

CMD npm start
CMD yarn start
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@
]
},
"engines": {
"node": "10.x"
"node": "14.x"
},
"volta": {
"node": "12.22.12"
"node": "14.21.2"
}
}
20 changes: 0 additions & 20 deletions src/common/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,25 +336,6 @@ function partialMatch (filter, value) {
}
}

/**
* Perform validation on phases.
* @param {Array} phases the phases data.
*/
async function validatePhases (phases) {
if (!phases || phases.length === 0) {
return
}
const records = await scan('Phase')
const map = new Map()
_.each(records, r => {
map.set(r.id, r)
})
const invalidPhases = _.filter(phases, p => !map.has(p.phaseId))
if (invalidPhases.length > 0) {
throw new errors.BadRequestError(`The following phases are invalid: ${toString(invalidPhases)}`)
}
}

/**
* Download file from S3
* @param {String} bucket the bucket name
Expand Down Expand Up @@ -1294,7 +1275,6 @@ module.exports = {
scanAll,
validateDuplicate,
partialMatch,
validatePhases,
downloadFromFileStack,
downloadFromS3,
deleteFromS3,
Expand Down
179 changes: 179 additions & 0 deletions src/common/phase-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
const _ = require('lodash')
const uuid = require('uuid/v4')
const moment = require('moment')

const errors = require('./errors')
const helper = require('./helper')

class ChallengePhaseHelper {
/**
* Populate challenge phases.
* @param {Array} phases the phases to populate
* @param {Date} startDate the challenge start date
* @param {String} timelineTemplateId the timeline template id
*/
async populatePhases (phases, startDate, timelineTemplateId) {
if (_.isUndefined(timelineTemplateId)) {
throw new errors.BadRequestError(`Invalid timeline template ID: ${timelineTemplateId}`)
}

const { timelineTempate, timelineTemplateMap } = await this.getTemplateAndTemplateMap(timelineTemplateId)
const { phaseDefinitionMap } = await this.getPhaseDefinitionsAndMap()

if (!phases || phases.length === 0) {
// auto populate phases
for (const p of timelineTempate) {
phases.push({ ...p })
}
}

for (const p of phases) {
const phaseDefinition = phaseDefinitionMap.get(p.phaseId)

p.id = uuid()
p.name = phaseDefinition.name
p.description = phaseDefinition.description

// set p.open based on current phase
const phaseTemplate = timelineTemplateMap.get(p.phaseId)
if (phaseTemplate) {
if (!p.duration) {
p.duration = phaseTemplate.defaultDuration
}

if (phaseTemplate.predecessor) {
const predecessor = _.find(phases, { phaseId: phaseTemplate.predecessor })
if (!predecessor) {
throw new errors.BadRequestError(`Predecessor ${phaseTemplate.predecessor} not found in given phases.`)
}
p.predecessor = phaseTemplate.predecessor
}
}
}

// calculate dates
if (!startDate) {
return
}

// sort phases by predecessor
phases.sort((a, b) => {
if (a.predecessor === b.phaseId) {
return 1
}
if (b.predecessor === a.phaseId) {
return -1
}
return 0
})

let isSubmissionPhaseOpen = false

for (let p of phases) {
const predecessor = timelineTemplateMap.get(p.predecessor)

if (predecessor == null) {
if (p.name === 'Registration') {
p.scheduledStartDate = moment(startDate).toDate()
}
if (p.name === 'Submission') {
if (p.scheduledStartDate != null) {
p.scheduledStartDate = moment(p.scheduledStartDate).toDate()
} else {
p.scheduledStartDate = moment(startDate).add(5, 'minutes').toDate()
}
}

if (moment(p.scheduledStartDate).isSameOrBefore(moment())) {
p.actualStartDate = p.scheduledStartDate
} else {
delete p.actualStartDate
}

p.scheduledEndDate = moment(p.scheduledStartDate).add(p.duration, 'seconds').toDate()
if (moment(p.scheduledEndDate).isBefore(moment())) {
delete p.actualEndDate
} else {
p.actualEndDate = p.scheduledEndDate
}
} else {
const precedecessorPhase = _.find(phases, { phaseId: predecessor.phaseId })
if (precedecessorPhase == null) {
throw new errors.BadRequestError(`Predecessor ${predecessor.phaseId} not found in given phases.`)
}
let phaseEndDate = moment(precedecessorPhase.scheduledEndDate)
if (precedecessorPhase.actualEndDate != null && moment(precedecessorPhase.actualEndDate).isAfter(phaseEndDate)) {
phaseEndDate = moment(precedecessorPhase.actualEndDate)
} else {
phaseEndDate = moment(precedecessorPhase.scheduledEndDate)
}

p.scheduledStartDate = phaseEndDate.toDate()
p.scheduledEndDate = moment(p.scheduledStartDate).add(p.duration, 'seconds').toDate()
}

p.isOpen = moment().isBetween(p.scheduledStartDate, p.scheduledEndDate)

if (p.isOpen) {
if (p.name === 'Submission') {
isSubmissionPhaseOpen = true
}
delete p.actualEndDate
}

if (moment(p.scheduledStartDate).isAfter(moment())) {
delete p.actualStartDate
delete p.actualEndDate
}

if (p.name === 'Post-Mortem' && isSubmissionPhaseOpen) {
delete p.actualStartDate
delete p.actualEndDate
p.isOpen = false
}
}

// phases.sort((a, b) => moment(a.scheduledStartDate).isAfter(b.scheduledStartDate))
}

async validatePhases (phases) {
if (!phases || phases.length === 0) {
return
}
const records = await helper.scan('Phase')
const map = new Map()
_.each(records, (r) => {
map.set(r.id, r)
})
const invalidPhases = _.filter(phases, (p) => !map.has(p.phaseId))
if (invalidPhases.length > 0) {
throw new errors.BadRequestError(
`The following phases are invalid: ${toString(invalidPhases)}`
)
}
}

async getPhaseDefinitionsAndMap () {
const records = await helper.scan('Phase')
const map = new Map()
_.each(records, (r) => {
map.set(r.id, r)
})
return { phaseDefinitions: records, phaseDefinitionMap: map }
}

async getTemplateAndTemplateMap (timelineTemplateId) {
const records = await helper.getById('TimelineTemplate', timelineTemplateId)
const map = new Map()
_.each(records.phases, (r) => {
map.set(r.phaseId, r)
})

return {
timelineTempate: records.phases,
timelineTemplateMap: map
}
}
}

module.exports = new ChallengePhaseHelper()
Loading