Skip to content

Commit 94f2cf6

Browse files
committed
feat: challenge phase extension
* move common phase functionality to a phase helper * remove post-mortem phase if submission phase is opened * determine isOpen status based on start and end dates
1 parent 56654ee commit 94f2cf6

File tree

5 files changed

+216
-57
lines changed

5 files changed

+216
-57
lines changed

.circleci/config.yml

+45-51
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
version: 2
22
defaults: &defaults
3-
docker:
4-
- image: cimg/python:3.11.0-browsers
3+
docker:
4+
- image: cimg/python:3.11.0-browsers
55
install_dependency: &install_dependency
66
name: Installation of build and deployment dependencies.
77
command: |
@@ -23,44 +23,38 @@ restore_cache_settings_for_build: &restore_cache_settings_for_build
2323
save_cache_settings: &save_cache_settings
2424
key: docker-node-modules-{{ checksum "yarn.lock" }}
2525
paths:
26-
- node_modules
26+
- node_modules
2727

2828
builddeploy_steps: &builddeploy_steps
29-
- checkout
30-
- setup_remote_docker
31-
- run: *install_dependency
32-
- run: *install_deploysuite
33-
- restore_cache: *restore_cache_settings_for_build
34-
- run:
35-
name: "Authenticate with CodeArtifact and build docker image"
36-
command: |
37-
./awsconfiguration.sh ${CODEARTIFACT_ENV}
38-
source awsenvconf
39-
aws codeartifact login --tool npm --repository topcoder-framework --domain topcoder --domain-owner $AWS_ACCOUNT_ID --region $AWS_REGION --namespace @topcoder-framework
40-
cp ~/.npmrc .
41-
rm -f awsenvconf
42-
./build.sh ${APPNAME}
43-
- save_cache: *save_cache_settings
44-
- deploy:
45-
name: Running MasterScript.
46-
command: |
47-
./awsconfiguration.sh $DEPLOY_ENV
48-
source awsenvconf
49-
./buildenv.sh -e $DEPLOY_ENV -b ${LOGICAL_ENV}-${APPNAME}-deployvar
50-
source buildenvvar
51-
./master_deploy.sh -d ECS -e $DEPLOY_ENV -t latest -s ${LOGICAL_ENV}-global-appvar,${LOGICAL_ENV}-${APPNAME}-appvar -i ${APPNAME}
29+
- checkout
30+
- setup_remote_docker
31+
- run: *install_dependency
32+
- run: *install_deploysuite
33+
- restore_cache: *restore_cache_settings_for_build
34+
- run:
35+
name: "Authenticate with CodeArtifact and build docker image"
36+
command: "./awsconfiguration.sh ${CODEARTIFACT_ENV}\nsource awsenvconf\naws codeartifact login --tool npm --repository topcoder-framework --domain topcoder --domain-owner $AWS_ACCOUNT_ID --region $AWS_REGION --namespace @topcoder-framework\ncp ~/.npmrc .\nrm -f awsenvconf \n./build.sh ${APPNAME}\n"
37+
- save_cache: *save_cache_settings
38+
- deploy:
39+
name: Running MasterScript.
40+
command: |
41+
./awsconfiguration.sh $DEPLOY_ENV
42+
source awsenvconf
43+
./buildenv.sh -e $DEPLOY_ENV -b ${LOGICAL_ENV}-${APPNAME}-deployvar
44+
source buildenvvar
45+
./master_deploy.sh -d ECS -e $DEPLOY_ENV -t latest -s ${LOGICAL_ENV}-global-appvar,${LOGICAL_ENV}-${APPNAME}-appvar -i ${APPNAME}
5246
jobs:
5347
# Build & Deploy against development backend
5448
"build-dev":
55-
<<: *defaults
49+
!!merge <<: *defaults
5650
environment:
5751
DEPLOY_ENV: "DEV"
5852
LOGICAL_ENV: "dev"
5953
APPNAME: "challenge-api"
6054
steps: *builddeploy_steps
6155

6256
"build-qa":
63-
<<: *defaults
57+
!!merge <<: *defaults
6458
environment:
6559
DEPLOY_ENV: "QA"
6660
LOGICAL_ENV: "qa"
@@ -69,36 +63,36 @@ jobs:
6963
steps: *builddeploy_steps
7064

7165
"build-prod":
72-
<<: *defaults
66+
!!merge <<: *defaults
7367
environment:
7468
DEPLOY_ENV: "PROD"
75-
LOGICAL_ENV: "prod"
76-
APPNAME: "challenge-api"
69+
LOGICAL_ENV: "prod"
70+
APPNAME: "challenge-api"
7771
steps: *builddeploy_steps
7872

7973
workflows:
8074
version: 2
8175
build:
8276
jobs:
83-
# Development builds are executed on "develop" branch only.
84-
- "build-dev":
85-
context : org-global
86-
filters:
87-
branches:
88-
only:
89-
- dev
77+
# Development builds are executed on "develop" branch only.
78+
- "build-dev":
79+
context: org-global
80+
filters:
81+
branches:
82+
only:
83+
- dev
9084

91-
- "build-qa":
92-
context : org-global
93-
filters:
94-
branches:
95-
only:
96-
- refactor/domain-challenge
85+
- "build-qa":
86+
context: org-global
87+
filters:
88+
branches:
89+
only:
90+
- refactor/domain-challenge
9791

98-
# Production builds are exectuted only on tagged commits to the
99-
# master branch.
100-
- "build-prod":
101-
context : org-global
102-
filters:
103-
branches:
104-
only: master
92+
# Production builds are exectuted only on tagged commits to the
93+
# master branch.
94+
- "build-prod":
95+
context: org-global
96+
filters:
97+
branches:
98+
only: master

.vscode/settings.json

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
{
2-
"editor.defaultFormatter": "standard.vscode-standard",
3-
"standard.autoFixOnSave": true,
4-
"[javascript]": {
5-
"editor.defaultFormatter": "esbenp.prettier-vscode"
6-
},
7-
"standard.autoFixOnSave": true
2+
"standard.autoFixOnSave": true,
3+
"[javascript]": {
4+
"editor.defaultFormatter": "esbenp.prettier-vscode"
5+
}
86
}

src/common/phase-helper.js

+147
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<<<<<<< HEAD
12
const { GRPC_CHALLENGE_SERVER_HOST, GRPC_CHALLENGE_SERVER_PORT } = process.env;
23

34
const {
@@ -22,6 +23,14 @@ const timelineTemplateService = require("../services/TimelineTemplateService");
2223
// );
2324

2425
const phaseDomain = new PhaseDomain(GRPC_CHALLENGE_SERVER_HOST, GRPC_CHALLENGE_SERVER_PORT);
26+
=======
27+
const _ = require('lodash')
28+
const uuid = require('uuid/v4')
29+
const moment = require('moment')
30+
31+
const errors = require('./errors')
32+
const helper = require('./helper')
33+
>>>>>>> 21bc5fc (feat: challenge phase extension)
2534

2635
class ChallengePhaseHelper {
2736
/**
@@ -30,6 +39,7 @@ class ChallengePhaseHelper {
3039
* @param {Date} startDate the challenge start date
3140
* @param {String} timelineTemplateId the timeline template id
3241
*/
42+
<<<<<<< HEAD
3343
async populatePhases(phases, startDate, timelineTemplateId) {
3444
if (_.isUndefined(timelineTemplateId)) {
3545
throw new errors.BadRequestError(`Invalid timeline template ID: ${timelineTemplateId}`);
@@ -44,10 +54,26 @@ class ChallengePhaseHelper {
4454
// auto populate phases
4555
for (const p of timelineTempate) {
4656
phases.push({ ...p });
57+
=======
58+
async populatePhases (phases, startDate, timelineTemplateId) {
59+
console.log('populatePhases', phases, startDate, timelineTemplateId)
60+
if (_.isUndefined(timelineTemplateId)) {
61+
throw new errors.BadRequestError(`Invalid timeline template ID: ${timelineTemplateId}`)
62+
}
63+
64+
const { timelineTempate, timelineTemplateMap } = await this.getTemplateAndTemplateMap(timelineTemplateId)
65+
const { phaseDefinitionMap } = await this.getPhaseDefinitionsAndMap()
66+
67+
if (!phases || phases.length === 0) {
68+
// auto populate phases
69+
for (const p of timelineTempate.phases) {
70+
phases.push({ ...p })
71+
>>>>>>> 21bc5fc (feat: challenge phase extension)
4772
}
4873
}
4974

5075
for (const p of phases) {
76+
<<<<<<< HEAD
5177
const phaseDefinition = phaseDefinitionMap.get(p.phaseId);
5278

5379
// TODO: move to domain-challenge
@@ -73,18 +99,45 @@ class ChallengePhaseHelper {
7399
}
74100
p.predecessor = phaseTemplate.predecessor;
75101
console.log("Setting predecessor", p.predecessor, "for phase", p.phaseId);
102+
=======
103+
const phaseDefinition = phaseDefinitionMap.get(p.phaseId)
104+
105+
p.id = uuid()
106+
p.name = phaseDefinition.name
107+
p.description = phaseDefinition.description
108+
109+
// set p.open based on current phase
110+
const phaseTemplate = timelineTemplateMap.get(p.phaseId)
111+
if (phaseTemplate) {
112+
if (!p.duration) {
113+
p.duration = phaseTemplate.defaultDuration
114+
}
115+
116+
if (phaseTemplate.predecessor) {
117+
const predecessor = _.find(phases, { phaseId: phaseTemplate.predecessor })
118+
if (!predecessor) {
119+
throw new errors.BadRequestError(`Predecessor ${phaseTemplate.predecessor} not found in given phases.`)
120+
}
121+
p.predecessor = phaseTemplate.predecessor
122+
console.log('Setting predecessor', p.predecessor, 'for phase', p.phaseId)
123+
>>>>>>> 21bc5fc (feat: challenge phase extension)
76124
}
77125
}
78126
}
79127

80128
// calculate dates
81129
if (!startDate) {
130+
<<<<<<< HEAD
82131
return;
132+
=======
133+
return
134+
>>>>>>> 21bc5fc (feat: challenge phase extension)
83135
}
84136

85137
// sort phases by predecessor
86138
phases.sort((a, b) => {
87139
if (a.predecessor === b.phaseId) {
140+
<<<<<<< HEAD
88141
return 1;
89142
}
90143
if (b.predecessor === a.phaseId) {
@@ -206,3 +259,97 @@ class ChallengePhaseHelper {
206259
}
207260

208261
module.exports = new ChallengePhaseHelper();
262+
=======
263+
return 1
264+
}
265+
if (b.predecessor === a.phaseId) {
266+
return -1
267+
}
268+
return 0
269+
})
270+
271+
let isSubmissionPhaseOpen = false
272+
let postMortemPhaseIndex = -1
273+
274+
for (let p of phases) {
275+
const predecessor = timelineTemplateMap.get(p.predecessor)
276+
277+
if (predecessor == null) {
278+
p.scheduledStartDate = startDate
279+
// p.actualStartDate = startDate
280+
281+
p.scheduledEndDate = moment(p.actualStartDate != null ? p.actaulStartDate : startDate).add(p.duration, 'seconds').toDate()
282+
} else {
283+
const precedecessorPhase = _.find(phases, { phaseId: predecessor.phaseId })
284+
if (precedecessorPhase == null) {
285+
throw new errors.BadRequestError(`Predecessor ${predecessor.phaseId} not found in given phases.`)
286+
}
287+
let phaseEndDate = moment(precedecessorPhase.scheduledEndDate)
288+
if (precedecessorPhase.actualEndDate != null && moment(precedecessorPhase.actualEndDate).isAfter(phaseEndDate)) {
289+
phaseEndDate = moment(precedecessorPhase.actualEndDate)
290+
} else {
291+
phaseEndDate = moment(precedecessorPhase.scheduledEndDate)
292+
}
293+
294+
p.scheduledStartDate = phaseEndDate.toDate()
295+
p.scheduledEndDate = moment(p.scheduledStartDate).add(p.duration, 'seconds').toDate()
296+
}
297+
p.isOpen = moment().isBetween(p.scheduledStartDate, p.scheduledEndDate)
298+
if (p.name === 'Submission') {
299+
isSubmissionPhaseOpen = p.isOpen
300+
}
301+
if (p.name === 'Post-Mortem') {
302+
postMortemPhaseIndex = _.findIndex(phases, { phaseId: p.phaseId })
303+
}
304+
}
305+
306+
// if submission phase is open, remove post-mortem phase
307+
if (isSubmissionPhaseOpen && postMortemPhaseIndex > -1) {
308+
phases.splice(postMortemPhaseIndex, 1)
309+
}
310+
311+
// phases.sort((a, b) => moment(a.scheduledStartDate).isAfter(b.scheduledStartDate))
312+
}
313+
314+
async validatePhases (phases) {
315+
if (!phases || phases.length === 0) {
316+
return
317+
}
318+
const records = await helper.scan('Phase')
319+
const map = new Map()
320+
_.each(records, (r) => {
321+
map.set(r.id, r)
322+
})
323+
const invalidPhases = _.filter(phases, (p) => !map.has(p.phaseId))
324+
if (invalidPhases.length > 0) {
325+
throw new errors.BadRequestError(
326+
`The following phases are invalid: ${toString(invalidPhases)}`
327+
)
328+
}
329+
}
330+
331+
async getPhaseDefinitionsAndMap () {
332+
const records = await helper.scan('Phase')
333+
const map = new Map()
334+
_.each(records, (r) => {
335+
map.set(r.id, r)
336+
})
337+
return { phaseDefinitions: records, phaseDefinitionMap: map }
338+
}
339+
340+
async getTemplateAndTemplateMap (timelineTemplateId) {
341+
const records = await helper.getById('TimelineTemplate', timelineTemplateId)
342+
const map = new Map()
343+
_.each(records.phases, (r) => {
344+
map.set(r.phaseId, r)
345+
})
346+
347+
return {
348+
timelineTempate: records.phases,
349+
timelineTemplateMap: map
350+
}
351+
}
352+
}
353+
354+
module.exports = new ChallengePhaseHelper()
355+
>>>>>>> 21bc5fc (feat: challenge phase extension)

src/services/ChallengeService.js

+17
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@
33
*/
44

55
const { GRPC_CHALLENGE_SERVER_HOST, GRPC_CHALLENGE_SERVER_PORT } = process.env;
6+
const _ = require("lodash");
7+
const Joi = require("joi");
8+
const uuid = require("uuid/v4");
9+
const config = require("config");
10+
const xss = require("xss");
11+
const helper = require("../common/helper");
12+
const logger = require("../common/logger");
13+
const errors = require("../common/errors");
14+
const phaseHelper = require("../common/phase-helper");
15+
const constants = require("../../app-constants");
16+
const models = require("../models");
17+
const HttpStatus = require("http-status-codes");
18+
const moment = require("moment");
19+
const PhaseService = require("./PhaseService");
20+
const ChallengeTypeService = require("./ChallengeTypeService");
21+
const ChallengeTrackService = require("./ChallengeTrackService");
22+
const ChallengeTimelineTemplateService = require("./ChallengeTimelineTemplateService");
623

724
const {
825
DomainHelper: { getLookupCriteria, getScanCriteria },

src/services/TimelineTemplateService.js

+3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ searchTimelineTemplates.schema = {
6060
* @returns {Object} the created timeline template
6161
*/
6262
async function createTimelineTemplate(timelineTemplate) {
63+
// await helper.validateDuplicate('TimelineTemplate', 'name', timelineTemplate.name)
64+
// await phaseHelper.validatePhases(timelineTemplate.phases)
65+
6366
const scanCriteria = getScanCriteria({
6467
name: timelineTemplate.name,
6568
});

0 commit comments

Comments
 (0)