From 13aa9ad4565eb230288616bb3a57108b9b2dae44 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 22 Mar 2023 23:24:02 +0300 Subject: [PATCH 1/2] feat: new phase logic --- src/common/phase-helper.js | 119 +++++++++++++++++++++++++++++++ src/services/ChallengeService.js | 62 +++++++--------- 2 files changed, 145 insertions(+), 36 deletions(-) diff --git a/src/common/phase-helper.js b/src/common/phase-helper.js index f29f6a71..1ac35215 100644 --- a/src/common/phase-helper.js +++ b/src/common/phase-helper.js @@ -160,6 +160,125 @@ class ChallengePhaseHelper { } } + async populatePhasesForChallengeCreation(phases, startDate, timelineTemplateId) { + if (_.isUndefined(timelineTemplateId)) { + throw new errors.BadRequestError(`Invalid timeline template ID: ${timelineTemplateId}`); + } + const { timelineTempate } = await this.getTemplateAndTemplateMap(timelineTemplateId); + const { phaseDefinitionMap } = await this.getPhaseDefinitionsAndMap(); + const finalPhases = _.map(timelineTempate, (phaseFromTemplate) => { + const phaseDefinition = phaseDefinitionMap.get(phaseFromTemplate.phaseId); + const phaseFromInput = _.find(phases, (p) => p.phaseId === phaseFromTemplate.phaseId); + const phase = { + id: uuid(), + phaseId: phaseFromTemplate.phaseId, + name: phaseDefinition.name, + description: phaseDefinition.description, + duration: _.defaultTo(_.get(phaseFromInput, "duration"), phaseFromTemplate.defaultDuration), + isOpen: false, + predecessor: phaseFromTemplate.predecessor, + constraints: _.defaultTo(_.get(phaseFromInput, "constraints"), []), + scheduledStartDate: undefined, + scheduledEndDate: undefined, + actualStartDate: undefined, + actualEndDate: undefined, + }; + if (_.isUndefined(phase.predecessor)) { + if (_.isUndefined(_.get(phaseFromInput, "scheduledStartDate"))) { + phase.scheduledStartDate = moment(startDate).toDate(); + } else { + phase.scheduledStartDate = moment(_.get(phaseFromInput, "scheduledStartDate")).toDate(); + } + phase.scheduledEndDate = moment(phase.scheduledStartDate) + .add(phase.duration, "seconds") + .toDate(); + } + return phase; + }); + for (let phase of finalPhases) { + if (_.isUndefined(phase.predecessor)) { + continue; + } + const precedecessorPhase = _.find(finalPhases, { + phaseId: phase.predecessor, + }); + if (phase.name === "Iterative Review Phase") { + phase.scheduledStartDate = precedecessorPhase.scheduledStartDate; + } else { + phase.scheduledStartDate = precedecessorPhase.scheduledEndDate; + } + phase.scheduledEndDate = moment(phase.scheduledStartDate) + .add(phase.duration, "seconds") + .toDate(); + } + return finalPhases; + } + + async populatePhasesForChallengeUpdate(challengePhases, newPhases, isBeingActivated) { + const { timelineTempate, timelineTemplateMap } = await this.getTemplateAndTemplateMap( + timelineTemplateId + ); + const { phaseDefinitionMap } = await this.getPhaseDefinitionsAndMap(); + + const updatedPhases = _.map(challengePhases, (phase) => { + const phaseFromTemplate = timelineTemplateMap.get(phase.phaseId); + const phaseDefinition = phaseDefinitionMap.get(phase.phaseId); + const updatedPhase = { + ...phase, + predecessor: phaseFromTemplate.predecessor, + description: phaseDefinition.description, + }; + if (!_.isUndefined(phase.actualEndDate)) { + return updatedPhase; + } + if (phase.name === "Iterative Review Phase") { + return updatedPhase; + } + const newPhase = _.find(newPhases, (p) => p.phaseId === phase.phaseId); + if (_.isUndefined(newPhase) && !isBeingActivated) { + return updatedPhase; + } + phase.duration = _.defaultTo(_.get(newPhase, "duration"), phase.duration); + if (_.isUndefined(phase.predecessor)) { + if ( + isBeingActivated && + moment( + _.defaultTo(_.get(newPhase, "scheduledStartDate"), phase.scheduledStartDate) + ).isSameOrBefore(moment()) + ) { + phase.isOpen = true; + phase.scheduledStartDate = moment().toDate(); + phase.actualStartDate = phase.scheduledStartDate; + } else if (phase.isOpen === false && !_.isUndefined(newPhase.scheduledStartDate)) { + phase.scheduledStartDate = moment(newPhase.scheduledStartDate).toDate(); + } + phase.scheduledEndDate = moment(phase.scheduledStartDate) + .add(phase.duration, "seconds") + .toDate(); + } + if (!_.isUndefined(newPhase) && !_.isUndefined(newPhase.constraints)) { + phase.constraints = newPhase.constraints; + } + return updatedPhase; + }); + for (let phase of updatedPhases) { + if (_.isUndefined(phase.predecessor)) { + continue; + } + if (phase.name === "Iterative Review Phase") { + continue; + } + const precedecessorPhase = _.find(updatedPhases, { + phaseId: phase.predecessor, + }); + phase.scheduledStartDate = precedecessorPhase.scheduledEndDate; + phase.scheduledEndDate = moment(phase.scheduledStartDate) + .add(phase.duration, "seconds") + .toDate(); + } + return updatedPhases; + } + async validatePhases(phases) { if (!phases || phases.length === 0) { return; diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index 3e96fb7c..e84f0cba 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1146,17 +1146,12 @@ async function createChallenge(currentUser, challenge, userToken) { } else { throw new errors.BadRequestError(`trackId and typeId are required to create a challenge`); } - } else { - if (challenge.phases == null) { - challenge.phases = []; - } - - await phaseHelper.populatePhases( - challenge.phases, - challenge.startDate, - challenge.timelineTemplateId - ); } + challenge.phases = await phaseHelper.populatePhasesForChallengeCreation( + challenge.phases, + challenge.startDate, + challenge.timelineTemplateId + ); // populate challenge terms // const projectTerms = await helper.getProjectDefaultTerms(challenge.projectId) @@ -1708,6 +1703,7 @@ async function update(currentUser, challengeId, data, isFull) { }); } } + let isChallengeBeingActivated = false; if (data.status) { if (data.status === constants.challengeStatuses.Active) { if ( @@ -1728,6 +1724,9 @@ async function update(currentUser, challengeId, data, isFull) { "Cannot Activate this project, it has no active billing account." ); } + if (challenge.status === constants.challengeStatuses.Draft) { + isChallengeBeingActivated = true; + } } if ( data.status === constants.challengeStatuses.CancelledRequirementsInfeasible || @@ -1910,6 +1909,7 @@ async function update(currentUser, challengeId, data, isFull) { // TODO: Fix this Tech Debt once legacy is turned off const finalStatus = data.status || challenge.status; const finalTimelineTemplateId = data.timelineTemplateId || challenge.timelineTemplateId; + const timelineTemplateChanged = false; if (!_.get(data, "legacy.pureV5") && !_.get(challenge, "legacy.pureV5")) { if ( finalStatus !== constants.challengeStatuses.New && @@ -1922,6 +1922,7 @@ async function update(currentUser, challengeId, data, isFull) { } else if (finalTimelineTemplateId !== challenge.timelineTemplateId) { // make sure there are no previous phases if the timeline template has changed challenge.phases = []; + timelineTemplateChanged = true; } if (data.prizeSets && data.prizeSets.length > 0) { @@ -1949,7 +1950,7 @@ async function update(currentUser, challengeId, data, isFull) { } } - if (data.phases || data.startDate) { + if (data.phases || data.startDate || timelineTemplateChanged) { if ( challenge.status === constants.challengeStatuses.Completed || challenge.status.indexOf(constants.challengeStatuses.Cancelled) > -1 @@ -1958,33 +1959,22 @@ async function update(currentUser, challengeId, data, isFull) { `Challenge phase/start date can not be modified for Completed or Cancelled challenges.` ); } - - if (data.phases && data.phases.length > 0) { - for (let i = 0; i < challenge.phases.length; i += 1) { - const updatedPhaseInfo = _.find( - data.phases, - (p) => p.phaseId === challenge.phases[i].phaseId - ); - if (updatedPhaseInfo) { - _.extend(challenge.phases[i], updatedPhaseInfo); - } - } - if (challenge.phases.length === 0 && data.phases && data.phases.length > 0) { - challenge.phases = data.phases; - } - } - - const newPhases = _.cloneDeep(challenge.phases) || []; const newStartDate = data.startDate || challenge.startDate; + let newPhases; + if (timelineTemplateChanged) { + newPhases = await phaseHelper.populatePhasesForChallengeCreation( + data.phases, + newStartDate, + finalTimelineTemplateId + ); + } else if (data.startDate || (data.phases && data.phases.length > 0)) { + newPhases = await phaseHelper.populatePhasesForChallengeUpdate( + challenge.phases, + data.phases, + isChallengeBeingActivated + ); + } - await PhaseService.validatePhases(newPhases); - - // populate phases - await phaseHelper.populatePhases( - newPhases, - newStartDate, - data.timelineTemplateId || challenge.timelineTemplateId - ); data.phases = newPhases; challenge.phases = newPhases; data.startDate = newStartDate; From d0862f942d4594f23a363157b25fa7b593ba27d0 Mon Sep 17 00:00:00 2001 From: eisbilir Date: Wed, 22 Mar 2023 23:26:54 +0300 Subject: [PATCH 2/2] fix: missing input --- src/common/phase-helper.js | 7 ++++++- src/services/ChallengeService.js | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common/phase-helper.js b/src/common/phase-helper.js index 1ac35215..bb010835 100644 --- a/src/common/phase-helper.js +++ b/src/common/phase-helper.js @@ -214,7 +214,12 @@ class ChallengePhaseHelper { return finalPhases; } - async populatePhasesForChallengeUpdate(challengePhases, newPhases, isBeingActivated) { + async populatePhasesForChallengeUpdate( + challengePhases, + newPhases, + timelineTemplateId, + isBeingActivated + ) { const { timelineTempate, timelineTemplateMap } = await this.getTemplateAndTemplateMap( timelineTemplateId ); diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index e84f0cba..334ff226 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -1971,6 +1971,7 @@ async function update(currentUser, challengeId, data, isFull) { newPhases = await phaseHelper.populatePhasesForChallengeUpdate( challenge.phases, data.phases, + challenge.timelineTemplateId, isChallengeBeingActivated ); }