Skip to content

Update phase dates, cache phases #601

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 7 commits into from
Mar 27, 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
211 changes: 38 additions & 173 deletions src/common/phase-helper.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const { GRPC_CHALLENGE_SERVER_HOST, GRPC_CHALLENGE_SERVER_PORT } = process.env;

const {
DomainHelper: { getLookupCriteria, getScanCriteria },
DomainHelper: { getScanCriteria },
} = require("@topcoder-framework/lib-common");

const { PhaseDomain, TimelineTemplateDomain } = require("@topcoder-framework/domain-challenge");
const { PhaseDomain } = require("@topcoder-framework/domain-challenge");

const _ = require("lodash");

Expand All @@ -13,153 +13,13 @@ const moment = require("moment");

const errors = require("./errors");

const phaseService = require("../services/PhaseService");
const timelineTemplateService = require("../services/TimelineTemplateService");

const phaseDomain = new PhaseDomain(GRPC_CHALLENGE_SERVER_HOST, GRPC_CHALLENGE_SERVER_PORT);

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);

// TODO: move to domain-challenge
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;
console.log("Setting predecessor", p.predecessor, "for phase", p.phaseId);
}
}
}

// 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") {
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;
}

if (p.constraints == null) {
p.constraints = [];
}
}
}

phaseDefinitionMap = {};
timelineTemplateMap = {};
async populatePhasesForChallengeCreation(phases, startDate, timelineTemplateId) {
if (_.isUndefined(timelineTemplateId)) {
throw new errors.BadRequestError(`Invalid timeline template ID: ${timelineTemplateId}`);
Expand Down Expand Up @@ -232,9 +92,7 @@ class ChallengePhaseHelper {
timelineTemplateId,
isBeingActivated
) {
const { timelineTempate, timelineTemplateMap } = await this.getTemplateAndTemplateMap(
timelineTemplateId
);
const { timelineTemplateMap } = await this.getTemplateAndTemplateMap(timelineTemplateId);
const { phaseDefinitionMap } = await this.getPhaseDefinitionsAndMap();
let fixedStartDate = undefined;
const updatedPhases = _.map(challengePhases, (phase) => {
Expand Down Expand Up @@ -298,13 +156,15 @@ class ChallengePhaseHelper {
}
iterativeReviewSet = true;
}
} else {
} else if (_.isUndefined(phase.actualStartDate)) {
phase.scheduledStartDate = predecessorPhase.scheduledEndDate;
}
phase.scheduledEndDate = moment(phase.scheduledStartDate)
.add(phase.duration, "seconds")
.toDate()
.toISOString();
if (_.isUndefined(phase.actualEndDate)) {
phase.scheduledEndDate = moment(phase.scheduledStartDate)
.add(phase.duration, "seconds")
.toDate()
.toISOString();
}
}
return updatedPhases;
}
Expand All @@ -313,41 +173,46 @@ class ChallengePhaseHelper {
if (!phases || phases.length === 0) {
return;
}
const { items: records } = await phaseDomain.scan({ criteria: getScanCriteria({}) });
const map = new Map();
_.each(records, (r) => {
map.set(r.id, r);
});
const invalidPhases = _.filter(phases, (p) => !map.has(p.phaseId));
const { phaseDefinitionMap } = await this.getPhaseDefinitionsAndMap();
const invalidPhases = _.filter(phases, (p) => !phaseDefinitionMap.has(p.phaseId));
if (invalidPhases.length > 0) {
throw new errors.BadRequestError(
`The following phases are invalid: ${toString(invalidPhases)}`
);
}
}

async getPhase(phaseId) {
const { phaseDefinitionMap } = await this.getPhaseDefinitionsAndMap();
return phaseDefinitionMap.get(phaseId);
}

async getPhaseDefinitionsAndMap() {
const { result: records } = await phaseService.searchPhases();
if (_.isEmpty(this.phaseDefinitionMap)) {
const { items: records } = await phaseDomain.scan({ criteria: getScanCriteria({}) });

const map = new Map();
_.each(records, (r) => {
map.set(r.id, r);
});
return { phaseDefinitions: records, phaseDefinitionMap: map };
const map = new Map();
_.each(records, (r) => {
map.set(r.id, r);
});

this.phaseDefinitionMap = { phaseDefinitions: records, phaseDefinitionMap: map };
}
return this.phaseDefinitionMap;
}

async getTemplateAndTemplateMap(timelineTemplateId) {
const records = await timelineTemplateService.getTimelineTemplate(timelineTemplateId);
if (_.isEmpty(this.timelineTemplateMap)) {
const records = await timelineTemplateService.getTimelineTemplate(timelineTemplateId);

const map = new Map();
_.each(records.phases, (r) => {
map.set(r.phaseId, r);
});
const map = new Map();
_.each(records.phases, (r) => {
map.set(r.phaseId, r);
});

return {
timelineTempate: records.phases,
timelineTemplateMap: map,
};
this.timelineTemplateMap = { timelineTempate: records.phases, timelineTemplateMap: map };
}
return this.timelineTemplateMap;
}
}

Expand Down
28 changes: 17 additions & 11 deletions src/services/ChallengeService.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,9 @@ const logger = require("../common/logger");
const errors = require("../common/errors");
const constants = require("../../app-constants");
const HttpStatus = require("http-status-codes");
const moment = require("moment");
const PhaseService = require("./PhaseService");
const ChallengeTypeService = require("./ChallengeTypeService");
const ChallengeTrackService = require("./ChallengeTrackService");
const ChallengeTimelineTemplateService = require("./ChallengeTimelineTemplateService");
const TimelineTemplateService = require("./TimelineTemplateService");
const { BadRequestError } = require("../common/errors");

const phaseHelper = require("../common/phase-helper");
Expand All @@ -34,7 +31,7 @@ const { Metadata: GrpcMetadata } = require("@grpc/grpc-js");

const esClient = helper.getESClient();

const { ChallengeDomain, UpdateChallengeInput } = require("@topcoder-framework/domain-challenge");
const { ChallengeDomain } = require("@topcoder-framework/domain-challenge");
const { hasAdminRole } = require("../common/role-helper");
const {
validateChallengeUpdateRequest,
Expand Down Expand Up @@ -1054,7 +1051,7 @@ async function createChallenge(currentUser, challenge, userToken) {
}

if (challenge.phases && challenge.phases.length > 0) {
await PhaseService.validatePhases(challenge.phases);
await phaseHelper.validatePhases(challenge.phases);
}

// populate phases
Expand Down Expand Up @@ -1274,7 +1271,7 @@ createChallenge.schema = {
*/
async function getPhasesAndPopulate(data) {
_.each(data.phases, async (p) => {
const phase = await PhaseService.getPhase(p.phaseId);
const phase = await phaseHelper.getPhase(p.phaseId);
p.name = phase.name;
if (phase.description) {
p.description = phase.description;
Expand Down Expand Up @@ -1724,7 +1721,12 @@ async function updateChallenge(currentUser, challengeId, data) {
}
}

if (data.phases || data.startDate || timelineTemplateChanged) {
let phasesUpdated = false;
if (
(data.phases && data.phases.length > 0) ||
isChallengeBeingActivated ||
timelineTemplateChanged
) {
if (
challenge.status === constants.challengeStatuses.Completed ||
challenge.status.indexOf(constants.challengeStatuses.Cancelled) > -1
Expand All @@ -1741,18 +1743,22 @@ async function updateChallenge(currentUser, challengeId, data) {
newStartDate,
finalTimelineTemplateId
);
} else if (data.startDate || (data.phases && data.phases.length > 0)) {
} else {
newPhases = await phaseHelper.populatePhasesForChallengeUpdate(
challenge.phases,
data.phases,
challenge.timelineTemplateId,
isChallengeBeingActivated
);
}

phasesUpdated = true;
data.phases = newPhases;
data.startDate = convertToISOString(newStartDate);
data.endDate = helper.calculateChallengeEndDate(challenge, data);
}
if (phasesUpdated || data.startDate) {
data.startDate = convertToISOString(_.min(_.map(data.phases, "scheduledStartDate")));
}
if (phasesUpdated || data.endDate) {
data.endDate = convertToISOString(_.max(_.map(data.phases, "scheduledEndDate")));
}

if (data.winners && data.winners.length && data.winners.length > 0) {
Expand Down