Skip to content

Commit 24849e1

Browse files
committed
Updates for manual phase manipulation
1 parent bd761e7 commit 24849e1

File tree

2 files changed

+121
-1
lines changed

2 files changed

+121
-1
lines changed

src/services/ChallengePhaseService.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,61 @@ async function hasPendingScorecardsForPhase(challengePhaseId) {
6262
return Number(count) > 0;
6363
}
6464

65+
async function hasCompletedReviewsForPhase(challengePhaseIdOrIds) {
66+
if (!config.REVIEW_DB_URL) {
67+
logger.debug(
68+
`Skipping completed review check for phase ${challengePhaseIdOrIds} because REVIEW_DB_URL is not configured`
69+
);
70+
return false;
71+
}
72+
73+
const phaseIds = Array.isArray(challengePhaseIdOrIds)
74+
? challengePhaseIdOrIds.filter((id) => !_.isNil(id))
75+
: [challengePhaseIdOrIds];
76+
77+
if (phaseIds.length === 0) {
78+
return false;
79+
}
80+
81+
const reviewPrisma = getReviewClient();
82+
const reviewSchema = config.REVIEW_DB_SCHEMA;
83+
const reviewTable = Prisma.raw(`"${reviewSchema}"."review"`);
84+
const statusText = Prisma.raw(`"status"::text`);
85+
const completedStatuses = ["IN_PROGRESS", "COMPLETED"];
86+
87+
const phaseIdClause =
88+
phaseIds.length === 1
89+
? Prisma.sql`"phaseId" = ${phaseIds[0]}`
90+
: Prisma.sql`"phaseId" IN (${Prisma.join(
91+
phaseIds.map((phaseId) => Prisma.sql`${phaseId}`)
92+
)})`;
93+
94+
const completedStatusClause = Prisma.sql`${statusText} IN (${Prisma.join(
95+
completedStatuses.map((status) => Prisma.sql`${status}`)
96+
)})`;
97+
98+
let rows;
99+
try {
100+
rows = await reviewPrisma.$queryRaw(
101+
Prisma.sql`
102+
SELECT COUNT(*)::int AS count
103+
FROM ${reviewTable}
104+
WHERE ${phaseIdClause}
105+
AND ${completedStatusClause}
106+
`
107+
);
108+
} catch (err) {
109+
logger.error(
110+
`Failed to check completed reviews for phase(s) ${phaseIds.join(", ")}: ${err.message}`,
111+
err
112+
);
113+
throw err;
114+
}
115+
116+
const [{ count = 0 } = {}] = rows || [];
117+
return Number(count) > 0;
118+
}
119+
65120
async function checkChallengeExists(challengeId) {
66121
const challenge = await prisma.challenge.findUnique({ where: { id: challengeId } });
67122
if (!challenge) {
@@ -230,8 +285,47 @@ async function partiallyUpdateChallengePhase(currentUser, challengeId, id, data)
230285
`Cannot close ${phaseName} because there are still pending scorecards`
231286
);
232287
}
288+
if (!("actualEndDate" in data) || _.isNil(data.actualEndDate)) {
289+
data.actualEndDate = new Date();
290+
}
233291
}
234292
if (isReopeningPhase) {
293+
const phaseName = challengePhase.name;
294+
if (phaseName === "Submission" || phaseName === "Registration") {
295+
const hasCompletedReviews = await hasCompletedReviewsForPhase(challengePhase.id);
296+
if (hasCompletedReviews) {
297+
throw new errors.ForbiddenError(
298+
"Cannot reopen Submission/Registration phase because reviews are already in progress or completed"
299+
);
300+
}
301+
}
302+
303+
if (phaseName === "Checkpoint Submission") {
304+
const checkpointPhases = await prisma.challengePhase.findMany({
305+
where: {
306+
challengeId: challengePhase.challengeId,
307+
name: { in: ["Checkpoint Screening", "Checkpoint Review"] },
308+
},
309+
select: { id: true },
310+
});
311+
const checkpointPhaseIds = checkpointPhases.map((cp) => cp.id);
312+
if (checkpointPhaseIds.length > 0) {
313+
const hasCheckpointReviews = await hasCompletedReviewsForPhase(checkpointPhaseIds);
314+
if (hasCheckpointReviews) {
315+
throw new errors.ForbiddenError(
316+
"Cannot reopen Checkpoint Submission phase because Checkpoint Screening or Checkpoint Review reviews are already in progress or completed"
317+
);
318+
}
319+
}
320+
}
321+
322+
const hasActualStartDate = !_.isNil(challengePhase.actualStartDate);
323+
const hasActualEndDate = !_.isNil(challengePhase.actualEndDate);
324+
if (hasActualStartDate) {
325+
data.actualStartDate = challengePhase.actualStartDate;
326+
} else if (!("actualStartDate" in data) || _.isNil(data.actualStartDate)) {
327+
data.actualStartDate = new Date();
328+
}
235329
data.actualEndDate = null;
236330
}
237331

test/unit/ChallengePhaseService.test.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,18 +153,44 @@ describe('challenge phase service unit tests', () => {
153153
should.equal(new Date(challengePhase.scheduledEndDate).toISOString(), expectedScheduledEndDate)
154154
})
155155

156-
it('partially update challenge phase - reopening clears actual end date', async () => {
156+
it('partially update challenge phase - closing sets actual end date', async () => {
157+
await prisma.challengePhase.update({
158+
where: { id: data.challengePhase1Id },
159+
data: { isOpen: true, actualStartDate: new Date(), actualEndDate: null }
160+
})
161+
162+
const before = new Date()
163+
const challengePhase = await service.partiallyUpdateChallengePhase(authUser, data.challenge.id, data.challengePhase1Id, {
164+
isOpen: false
165+
})
166+
const after = new Date()
167+
168+
should.equal(challengePhase.isOpen, false)
169+
should.exist(challengePhase.actualEndDate)
170+
const actualEndMs = new Date(challengePhase.actualEndDate).getTime()
171+
actualEndMs.should.be.at.least(before.getTime())
172+
actualEndMs.should.be.at.most(after.getTime())
173+
})
174+
175+
it('partially update challenge phase - reopening clears actual end date and sets start date', async () => {
157176
const previousEndDate = new Date()
158177
await prisma.challengePhase.update({
159178
where: { id: data.challengePhase1Id },
160179
data: { isOpen: false, actualEndDate: previousEndDate }
161180
})
162181

182+
const before = new Date()
163183
const challengePhase = await service.partiallyUpdateChallengePhase(authUser, data.challenge.id, data.challengePhase1Id, {
164184
isOpen: true
165185
})
186+
const after = new Date()
187+
166188
should.equal(challengePhase.isOpen, true)
167189
should.equal(challengePhase.actualEndDate, null)
190+
should.exist(challengePhase.actualStartDate)
191+
const actualStartMs = new Date(challengePhase.actualStartDate).getTime()
192+
actualStartMs.should.be.at.least(before.getTime())
193+
actualStartMs.should.be.at.most(after.getTime())
168194
})
169195

170196
it('partially update challenge phase - not found', async () => {

0 commit comments

Comments
 (0)