mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-10-25 10:37:30 +08:00
feat(api): exam date use + split prisma files (#62344)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
co-authored by
Oliver Eyton-Williams
parent
3dc1e8b9d8
commit
568840b8d8
@@ -16,6 +16,7 @@ import {
|
||||
createSuperRequest,
|
||||
defaultUserId,
|
||||
devLogin,
|
||||
serializeDates,
|
||||
setupServer
|
||||
} from '../../../vitest.utils.js';
|
||||
import {
|
||||
@@ -102,6 +103,7 @@ describe('/exam-environment/', () => {
|
||||
examId,
|
||||
generatedExamId: mock.oid(),
|
||||
startTimeInMS: Date.now(),
|
||||
startTime: new Date(),
|
||||
userId: defaultUserId
|
||||
}
|
||||
});
|
||||
@@ -133,6 +135,7 @@ describe('/exam-environment/', () => {
|
||||
examId: mock.examId,
|
||||
generatedExamId: mock.oid(),
|
||||
startTimeInMS: Date.now() - (1000 * 60 * 60 * 2 + 1000),
|
||||
startTime: new Date(Date.now() - (1000 * 60 * 60 * 2 + 1000)),
|
||||
userId: defaultUserId
|
||||
}
|
||||
});
|
||||
@@ -164,6 +167,7 @@ describe('/exam-environment/', () => {
|
||||
examId: mock.examId,
|
||||
generatedExamId: mock.oid(),
|
||||
startTimeInMS: Date.now(),
|
||||
startTime: new Date(),
|
||||
userId: defaultUserId
|
||||
}
|
||||
});
|
||||
@@ -235,12 +239,13 @@ describe('/exam-environment/', () => {
|
||||
examId: mock.examId,
|
||||
generatedExamId: mock.generatedExam.id,
|
||||
startTimeInMS: Date.now(),
|
||||
startTime: new Date(),
|
||||
questionSets: []
|
||||
}
|
||||
});
|
||||
|
||||
const body: Static<typeof examEnvironmentPostExamAttempt.body> = {
|
||||
attempt: mock.examAttemptSansSubmissionTimeInMS
|
||||
attempt: mock.examAttemptSansSubmissionTime
|
||||
};
|
||||
|
||||
const res = await superPost('/exam-environment/exam/attempt')
|
||||
@@ -330,10 +335,13 @@ describe('/exam-environment/', () => {
|
||||
});
|
||||
|
||||
it('should return an error if the exam has been attempted too recently to retake', async () => {
|
||||
const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000;
|
||||
|
||||
const recentExamAttempt = {
|
||||
...mock.examAttempt,
|
||||
// Set start time such that exam has just expired
|
||||
startTimeInMS: Date.now() - mock.exam.config.totalTimeInMS
|
||||
startTimeInMS: Date.now() - examTotalTimeInMS,
|
||||
startTime: new Date(Date.now() - examTotalTimeInMS)
|
||||
};
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({
|
||||
data: recentExamAttempt
|
||||
@@ -357,6 +365,8 @@ describe('/exam-environment/', () => {
|
||||
}
|
||||
});
|
||||
|
||||
const examRetakeTimeInMS = mock.exam.config.retakeTimeInS * 1000;
|
||||
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.update({
|
||||
where: {
|
||||
id: recentExamAttempt.id
|
||||
@@ -364,9 +374,10 @@ describe('/exam-environment/', () => {
|
||||
data: {
|
||||
// Set start time such that exam has expired, but retake time -1s has passed
|
||||
startTimeInMS:
|
||||
Date.now() -
|
||||
(mock.exam.config.totalTimeInMS +
|
||||
(mock.exam.config.retakeTimeInMS - 1000))
|
||||
Date.now() - (examTotalTimeInMS + (examRetakeTimeInMS - 1000)),
|
||||
startTime: new Date(
|
||||
Date.now() - (examTotalTimeInMS + (examRetakeTimeInMS - 1000))
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -393,9 +404,14 @@ describe('/exam-environment/', () => {
|
||||
it('should use a new exam attempt if all previous attempts were started > 24 hours ago', async () => {
|
||||
const recentExamAttempt = structuredClone(mock.examAttempt);
|
||||
// Set start time such that exam has expired, but 24 hours + 1s has passed
|
||||
const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000;
|
||||
|
||||
recentExamAttempt.startTimeInMS =
|
||||
Date.now() -
|
||||
(mock.exam.config.totalTimeInMS + (24 * 60 * 60 * 1000 + 1000));
|
||||
Date.now() - examTotalTimeInMS + (24 * 60 * 60 * 1000 + 1000);
|
||||
recentExamAttempt.startTime = new Date(
|
||||
Date.now() - (examTotalTimeInMS + (24 * 60 * 60 * 1000 + 1000))
|
||||
);
|
||||
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({
|
||||
data: recentExamAttempt
|
||||
});
|
||||
@@ -450,7 +466,7 @@ describe('/exam-environment/', () => {
|
||||
expect(res).toMatchObject({
|
||||
status: 200,
|
||||
body: {
|
||||
examAttempt: latestAttempt
|
||||
examAttempt: serializeDates(latestAttempt)
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -458,12 +474,20 @@ describe('/exam-environment/', () => {
|
||||
it('should return an error if the database has insufficient generated exams', async () => {
|
||||
// Add completed attempt for generated exam
|
||||
const submittedAttempt = structuredClone(mock.examAttempt);
|
||||
const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000;
|
||||
// Long-enough ago to be considered "submitted", and not trigger cooldown
|
||||
submittedAttempt.startTimeInMS =
|
||||
Date.now() -
|
||||
24 * 60 * 60 * 1000 -
|
||||
mock.exam.config.totalTimeInMS -
|
||||
examTotalTimeInMS -
|
||||
1 * 60 * 60 * 1000;
|
||||
submittedAttempt.startTime = new Date(
|
||||
Date.now() -
|
||||
24 * 60 * 60 * 1000 -
|
||||
examTotalTimeInMS -
|
||||
1 * 60 * 60 * 1000
|
||||
);
|
||||
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({
|
||||
data: submittedAttempt
|
||||
});
|
||||
@@ -523,6 +547,8 @@ describe('/exam-environment/', () => {
|
||||
generatedExamId: generatedExam!.id,
|
||||
questionSets: [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
startTime: expect.any(Date),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
startTimeInMS: expect.any(Number),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
version: expect.any(Number)
|
||||
@@ -594,10 +620,12 @@ describe('/exam-environment/', () => {
|
||||
|
||||
const userExam = constructUserExam(generatedExam!, mock.exam);
|
||||
|
||||
expect(res.body).toMatchObject({
|
||||
examAttempt,
|
||||
exam: userExam
|
||||
});
|
||||
expect(res.body).toMatchObject(
|
||||
serializeDates({
|
||||
examAttempt,
|
||||
exam: userExam
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -638,7 +666,9 @@ describe('/exam-environment/', () => {
|
||||
name: mock.exam.config.name,
|
||||
note: mock.exam.config.note,
|
||||
passingPercent: mock.exam.config.passingPercent,
|
||||
totalTimeInS: mock.exam.config.totalTimeInS,
|
||||
totalTimeInMS: mock.exam.config.totalTimeInMS,
|
||||
retakeTimeInS: mock.exam.config.retakeTimeInS,
|
||||
retakeTimeInMS: mock.exam.config.retakeTimeInMS
|
||||
},
|
||||
id: mock.examId
|
||||
@@ -713,7 +743,9 @@ describe('/exam-environment/', () => {
|
||||
name: mock.exam.config.name,
|
||||
note: mock.exam.config.note,
|
||||
passingPercent: mock.exam.config.passingPercent,
|
||||
totalTimeInS: mock.exam.config.totalTimeInS,
|
||||
totalTimeInMS: mock.exam.config.totalTimeInMS,
|
||||
retakeTimeInS: mock.exam.config.retakeTimeInS,
|
||||
retakeTimeInMS: mock.exam.config.retakeTimeInMS
|
||||
},
|
||||
id: mock.examId
|
||||
@@ -725,10 +757,13 @@ describe('/exam-environment/', () => {
|
||||
|
||||
it("should indicate an exam's availability based on the last attempt's start time, and the exam retake time", async () => {
|
||||
// Create a recent exam attempt that's within the retake time
|
||||
const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000;
|
||||
|
||||
const recentExamAttempt = {
|
||||
...mock.examAttempt,
|
||||
userId: defaultUserId,
|
||||
startTimeInMS: Date.now() - mock.exam.config.totalTimeInMS
|
||||
startTimeInMS: Date.now() - examTotalTimeInMS,
|
||||
startTime: new Date(Date.now() - examTotalTimeInMS)
|
||||
};
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({
|
||||
data: recentExamAttempt
|
||||
@@ -742,15 +777,17 @@ describe('/exam-environment/', () => {
|
||||
expect(res.body).toMatchObject([{ canTake: false }]);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const examRetakeTimeInMS = mock.exam.config.retakeTimeInS * 1000;
|
||||
|
||||
// Update the attempt to be outside the retake time
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.update({
|
||||
where: { id: recentExamAttempt.id },
|
||||
data: {
|
||||
startTimeInMS:
|
||||
Date.now() -
|
||||
(mock.exam.config.totalTimeInMS +
|
||||
mock.exam.config.retakeTimeInMS +
|
||||
1000)
|
||||
Date.now() - (examTotalTimeInMS + examRetakeTimeInMS + 1000),
|
||||
startTime: new Date(
|
||||
Date.now() - (examTotalTimeInMS + examRetakeTimeInMS + 1000)
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -766,14 +803,16 @@ describe('/exam-environment/', () => {
|
||||
|
||||
it('should indicate an exam is unavailable if there are any pending moderation records for the exam attempts', async () => {
|
||||
// Create an exam attempt that's outside the retake time
|
||||
const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000;
|
||||
const examRetakeTimeInMS = mock.exam.config.retakeTimeInS * 1000;
|
||||
const examAttempt = {
|
||||
...mock.examAttempt,
|
||||
userId: defaultUserId,
|
||||
startTimeInMS:
|
||||
Date.now() -
|
||||
(mock.exam.config.totalTimeInMS +
|
||||
mock.exam.config.retakeTimeInMS +
|
||||
1000)
|
||||
Date.now() - (examTotalTimeInMS + examRetakeTimeInMS + 1000),
|
||||
startTime: new Date(
|
||||
Date.now() - (examTotalTimeInMS + examRetakeTimeInMS + 1000)
|
||||
)
|
||||
};
|
||||
const createdAttempt =
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({
|
||||
@@ -868,11 +907,12 @@ describe('/exam-environment/', () => {
|
||||
id: attempt.id,
|
||||
examId: mock.exam.id,
|
||||
result: null,
|
||||
startTime: attempt.startTime,
|
||||
startTimeInMS: attempt.startTimeInMS,
|
||||
questionSets: attempt.questionSets
|
||||
};
|
||||
|
||||
expect(res.body).toEqual(examEnvironmentExamAttempt);
|
||||
expect(res.body).toEqual(serializeDates(examEnvironmentExamAttempt));
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
@@ -908,17 +948,21 @@ describe('/exam-environment/', () => {
|
||||
id: attempt.id,
|
||||
examId: mock.exam.id,
|
||||
result: null,
|
||||
startTime: attempt.startTime,
|
||||
startTimeInMS: attempt.startTimeInMS,
|
||||
questionSets: attempt.questionSets
|
||||
};
|
||||
|
||||
expect(res.body).toEqual(examEnvironmentExamAttempt);
|
||||
expect(res.body).toEqual(serializeDates(examEnvironmentExamAttempt));
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('should return the attempt with results, if the attempt has been moderated', async () => {
|
||||
const examAttempt = structuredClone(mock.examAttempt);
|
||||
examAttempt.startTimeInMS = Date.now() - mock.exam.config.totalTimeInMS;
|
||||
const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000;
|
||||
|
||||
examAttempt.startTimeInMS = Date.now() - examTotalTimeInMS;
|
||||
examAttempt.startTime = new Date(Date.now() - examTotalTimeInMS);
|
||||
const attempt =
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({
|
||||
data: examAttempt
|
||||
@@ -945,11 +989,12 @@ describe('/exam-environment/', () => {
|
||||
score: 25,
|
||||
passingPercent: 80
|
||||
},
|
||||
startTime: attempt.startTime,
|
||||
startTimeInMS: attempt.startTimeInMS,
|
||||
questionSets: attempt.questionSets
|
||||
};
|
||||
|
||||
expect(res.body).toEqual(examEnvironmentExamAttempt);
|
||||
expect(res.body).toEqual(serializeDates(examEnvironmentExamAttempt));
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
});
|
||||
@@ -1000,11 +1045,12 @@ describe('/exam-environment/', () => {
|
||||
id: attempt.id,
|
||||
examId: mock.exam.id,
|
||||
result: null,
|
||||
startTime: attempt.startTime,
|
||||
startTimeInMS: attempt.startTimeInMS,
|
||||
questionSets: attempt.questionSets
|
||||
};
|
||||
|
||||
expect(res.body).toEqual([examEnvironmentExamAttempt]);
|
||||
expect(res.body).toEqual([serializeDates(examEnvironmentExamAttempt)]);
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
@@ -1030,17 +1076,21 @@ describe('/exam-environment/', () => {
|
||||
id: attempt.id,
|
||||
examId: mock.exam.id,
|
||||
result: null,
|
||||
startTime: attempt.startTime,
|
||||
startTimeInMS: attempt.startTimeInMS,
|
||||
questionSets: attempt.questionSets
|
||||
};
|
||||
|
||||
expect(res.body).toEqual([examEnvironmentExamAttempt]);
|
||||
expect(res.body).toEqual([serializeDates(examEnvironmentExamAttempt)]);
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('should return the attempts with results, if they have been moderated', async () => {
|
||||
const examAttempt = structuredClone(mock.examAttempt);
|
||||
examAttempt.startTimeInMS = Date.now() - mock.exam.config.totalTimeInMS;
|
||||
const examTotalTimeInMS = mock.exam.config.totalTimeInS * 1000;
|
||||
|
||||
examAttempt.startTimeInMS = Date.now() - examTotalTimeInMS;
|
||||
examAttempt.startTime = new Date(Date.now() - examTotalTimeInMS);
|
||||
const attempt =
|
||||
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({
|
||||
data: examAttempt
|
||||
@@ -1065,11 +1115,12 @@ describe('/exam-environment/', () => {
|
||||
score: 25,
|
||||
passingPercent: 80
|
||||
},
|
||||
startTime: attempt.startTime,
|
||||
startTimeInMS: attempt.startTimeInMS,
|
||||
questionSets: attempt.questionSets
|
||||
};
|
||||
|
||||
expect(res.body).toEqual([examEnvironmentExamAttempt]);
|
||||
expect(res.body).toEqual([serializeDates(examEnvironmentExamAttempt)]);
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
});
|
||||
@@ -1115,12 +1166,14 @@ describe('/exam-environment/', () => {
|
||||
id: attempt.id,
|
||||
examId: mock.exam.id,
|
||||
result: null,
|
||||
startTime: attempt.startTime,
|
||||
startTimeInMS: attempt.startTimeInMS,
|
||||
questionSets: attempt.questionSets,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
version: expect.any(Number)
|
||||
};
|
||||
expect(res.body).toEqual([examEnvironmentExamAttempt]);
|
||||
|
||||
expect(res.body).toEqual([serializeDates(examEnvironmentExamAttempt)]);
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -259,9 +259,13 @@ async function postExamGeneratedExamHandler(
|
||||
const examAttempts = maybeExamAttempts.data;
|
||||
|
||||
const lastAttempt = examAttempts.length
|
||||
? examAttempts.reduce((latest, current) =>
|
||||
latest.startTimeInMS > current.startTimeInMS ? latest : current
|
||||
)
|
||||
? examAttempts.reduce((latest, current) => {
|
||||
const latestStartTime =
|
||||
latest.startTime?.getTime() ?? latest.startTimeInMS;
|
||||
const currentStartTime =
|
||||
current.startTime?.getTime() ?? current.startTimeInMS;
|
||||
return latestStartTime > currentStartTime ? latest : current;
|
||||
})
|
||||
: null;
|
||||
|
||||
if (lastAttempt) {
|
||||
@@ -300,11 +304,19 @@ async function postExamGeneratedExamHandler(
|
||||
);
|
||||
}
|
||||
|
||||
const examExpirationTime =
|
||||
lastAttempt.startTimeInMS + exam.config.totalTimeInMS;
|
||||
const lastAttemptStartTime =
|
||||
lastAttempt.startTime?.getTime() ?? lastAttempt.startTimeInMS;
|
||||
const examTotalTimeInMS = exam.config.totalTimeInS
|
||||
? exam.config.totalTimeInS * 1000
|
||||
: exam.config.totalTimeInMS;
|
||||
const examExpirationTime = lastAttemptStartTime + examTotalTimeInMS;
|
||||
|
||||
if (examExpirationTime < Date.now()) {
|
||||
const examRetakeTimeInMS = exam.config.retakeTimeInS
|
||||
? exam.config.retakeTimeInS * 1000
|
||||
: exam.config.retakeTimeInMS;
|
||||
const retakeAllowed =
|
||||
examExpirationTime + exam.config.retakeTimeInMS < Date.now();
|
||||
examExpirationTime + examRetakeTimeInMS < Date.now();
|
||||
|
||||
if (!retakeAllowed) {
|
||||
logger.warn(
|
||||
@@ -444,6 +456,7 @@ async function postExamGeneratedExamHandler(
|
||||
examId: exam.id,
|
||||
generatedExamId: generatedExam.id,
|
||||
startTimeInMS: Date.now(),
|
||||
startTime: new Date(),
|
||||
questionSets: []
|
||||
}
|
||||
})
|
||||
@@ -540,9 +553,12 @@ async function postExamAttemptHandler(
|
||||
);
|
||||
}
|
||||
|
||||
const latestAttempt = attempts.reduce((latest, current) =>
|
||||
latest.startTimeInMS > current.startTimeInMS ? latest : current
|
||||
);
|
||||
const latestAttempt = attempts.reduce((latest, current) => {
|
||||
const latestStartTime = latest.startTime?.getTime() ?? latest.startTimeInMS;
|
||||
const currentStartTime =
|
||||
current.startTime?.getTime() ?? current.startTimeInMS;
|
||||
return latestStartTime > currentStartTime ? latest : current;
|
||||
});
|
||||
|
||||
const maybeExam = await mapErr(
|
||||
this.prisma.examEnvironmentExam.findUnique({
|
||||
@@ -573,8 +589,13 @@ async function postExamAttemptHandler(
|
||||
);
|
||||
}
|
||||
|
||||
const latestAttemptStartTime =
|
||||
latestAttempt.startTime?.getTime() ?? latestAttempt.startTimeInMS;
|
||||
const examTotalTimeInMS = exam.config.totalTimeInS
|
||||
? exam.config.totalTimeInS * 1000
|
||||
: exam.config.totalTimeInMS;
|
||||
const isAttemptExpired =
|
||||
latestAttempt.startTimeInMS + exam.config.totalTimeInMS < Date.now();
|
||||
latestAttemptStartTime + examTotalTimeInMS < Date.now();
|
||||
|
||||
if (isAttemptExpired) {
|
||||
logger.warn(
|
||||
@@ -715,7 +736,8 @@ async function getExams(
|
||||
select: {
|
||||
id: true,
|
||||
examId: true,
|
||||
startTimeInMS: true
|
||||
startTimeInMS: true,
|
||||
startTime: true
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -740,7 +762,9 @@ async function getExams(
|
||||
name: exam.config.name,
|
||||
note: exam.config.note,
|
||||
totalTimeInMS: exam.config.totalTimeInMS,
|
||||
totalTimeInS: exam.config.totalTimeInS,
|
||||
retakeTimeInMS: exam.config.retakeTimeInMS,
|
||||
retakeTimeInS: exam.config.retakeTimeInS,
|
||||
passingPercent: exam.config.passingPercent
|
||||
},
|
||||
canTake: false
|
||||
@@ -762,9 +786,13 @@ async function getExams(
|
||||
const attemptsForExam = attempts.filter(a => a.examId === exam.id);
|
||||
|
||||
const lastAttempt = attemptsForExam.length
|
||||
? attemptsForExam.reduce((latest, current) =>
|
||||
latest.startTimeInMS > current.startTimeInMS ? latest : current
|
||||
)
|
||||
? attemptsForExam.reduce((latest, current) => {
|
||||
const latestStartTime =
|
||||
latest.startTime?.getTime() ?? latest.startTimeInMS;
|
||||
const currentStartTime =
|
||||
current.startTime?.getTime() ?? current.startTimeInMS;
|
||||
return latestStartTime > currentStartTime ? latest : current;
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!lastAttempt) {
|
||||
@@ -774,10 +802,16 @@ async function getExams(
|
||||
continue;
|
||||
}
|
||||
|
||||
const lastAttemptStartTime =
|
||||
lastAttempt.startTime?.getTime() ?? lastAttempt.startTimeInMS;
|
||||
const examTotalTimeInMS = exam.config.totalTimeInS
|
||||
? exam.config.totalTimeInS * 1000
|
||||
: exam.config.totalTimeInMS;
|
||||
const examRetakeTimeInMS = exam.config.retakeTimeInS
|
||||
? exam.config.retakeTimeInS * 1000
|
||||
: exam.config.retakeTimeInMS;
|
||||
const retakeDateInMS =
|
||||
lastAttempt.startTimeInMS +
|
||||
exam.config.totalTimeInMS +
|
||||
exam.config.retakeTimeInMS;
|
||||
lastAttemptStartTime + examTotalTimeInMS + examRetakeTimeInMS;
|
||||
const isRetakeTimePassed = Date.now() > retakeDateInMS;
|
||||
|
||||
if (!isRetakeTimePassed) {
|
||||
|
||||
@@ -30,6 +30,7 @@ const examEnvAttempt = Type.Object({
|
||||
id: Type.String(),
|
||||
examId: Type.String(),
|
||||
startTimeInMS: Type.Number(),
|
||||
startTime: Type.String({ format: 'date-time' }),
|
||||
questionSets: Type.Array(
|
||||
Type.Object({
|
||||
id: Type.String(),
|
||||
@@ -37,7 +38,8 @@ const examEnvAttempt = Type.Object({
|
||||
Type.Object({
|
||||
id: Type.String(),
|
||||
answers: Type.Array(Type.String()),
|
||||
submissionTimeInMS: Type.Number()
|
||||
submissionTimeInMS: Type.Number(),
|
||||
submissionTime: Type.String({ format: 'date-time' })
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -12,7 +12,9 @@ export const examEnvironmentExams = {
|
||||
name: Type.String(),
|
||||
note: Type.String(),
|
||||
totalTimeInMS: Type.Number(),
|
||||
totalTimeInS: Type.Number(),
|
||||
retakeTimeInMS: Type.Number(),
|
||||
retakeTimeInS: Type.Number(),
|
||||
passingPercent: Type.Number()
|
||||
}),
|
||||
canTake: Type.Boolean()
|
||||
|
||||
@@ -239,7 +239,7 @@ describe('Exam Environment mocked Math.random', () => {
|
||||
const allQuestions = databaseAttemptQuestionSets.flatMap(
|
||||
qs => qs.questions
|
||||
);
|
||||
expect(allQuestions.every(q => q.submissionTimeInMS)).toBe(true);
|
||||
expect(allQuestions.every(q => q.submissionTime)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not change the submission time of any questions that have not changed', () => {
|
||||
@@ -264,7 +264,7 @@ describe('Exam Environment mocked Math.random', () => {
|
||||
userAttemptToDatabaseAttemptQuestionSets(userAttempt, latestAttempt);
|
||||
|
||||
const submissionTimes = databaseAttemptQuestionSets.flatMap(qs =>
|
||||
qs.questions.map(q => q.submissionTimeInMS)
|
||||
qs.questions.map(q => q.submissionTime)
|
||||
);
|
||||
|
||||
const sameAttempt = userAttemptToDatabaseAttemptQuestionSets(
|
||||
@@ -273,7 +273,7 @@ describe('Exam Environment mocked Math.random', () => {
|
||||
);
|
||||
|
||||
const sameSubmissionTimes = sameAttempt.flatMap(qs =>
|
||||
qs.questions.map(q => q.submissionTimeInMS)
|
||||
qs.questions.map(q => q.submissionTime)
|
||||
);
|
||||
|
||||
expect(submissionTimes).toEqual(sameSubmissionTimes);
|
||||
@@ -314,9 +314,9 @@ describe('Exam Environment mocked Math.random', () => {
|
||||
);
|
||||
|
||||
expect(
|
||||
newAttemptQuestionSets[0]?.questions[0]?.submissionTimeInMS
|
||||
newAttemptQuestionSets[0]?.questions[0]?.submissionTime
|
||||
).not.toEqual(
|
||||
databaseAttemptQuestionSets[0]?.questions[0]?.submissionTimeInMS
|
||||
databaseAttemptQuestionSets[0]?.questions[0]?.submissionTime
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -447,8 +447,10 @@ describe('Exam Environment Schema', () => {
|
||||
passingPercent: 0.0,
|
||||
questionSets: configQuestionSets,
|
||||
retakeTimeInMS: 0,
|
||||
retakeTimeInS: 0,
|
||||
tags,
|
||||
totalTimeInMS: 0
|
||||
totalTimeInMS: 0,
|
||||
totalTimeInS: 0
|
||||
};
|
||||
|
||||
const questions = [
|
||||
@@ -522,11 +524,17 @@ describe('Exam Environment Schema', () => {
|
||||
{
|
||||
id: oid(),
|
||||
questions: [
|
||||
{ answers: [oid()], id: oid(), submissionTimeInMS: 0 }
|
||||
{
|
||||
answers: [oid()],
|
||||
id: oid(),
|
||||
submissionTime: new Date(),
|
||||
submissionTimeInMS: Date.now()
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
startTimeInMS: 0,
|
||||
startTimeInMS: Date.now(),
|
||||
startTime: new Date(),
|
||||
userId: oid()
|
||||
}
|
||||
});
|
||||
|
||||
@@ -112,9 +112,11 @@ export function constructUserExam(
|
||||
|
||||
const config = {
|
||||
totalTimeInMS: exam.config.totalTimeInMS,
|
||||
totalTimeInS: exam.config.totalTimeInS,
|
||||
name: exam.config.name,
|
||||
note: exam.config.note,
|
||||
retakeTimeInMS: exam.config.retakeTimeInMS,
|
||||
retakeTimeInS: exam.config.retakeTimeInS,
|
||||
passingPercent: exam.config.passingPercent
|
||||
};
|
||||
|
||||
@@ -241,7 +243,11 @@ export function userAttemptToDatabaseAttemptQuestionSets(
|
||||
databaseAttemptQuestionSets.push({
|
||||
...questionSet,
|
||||
questions: questionSet.questions.map(q => {
|
||||
return { ...q, submissionTimeInMS: Date.now() };
|
||||
return {
|
||||
...q,
|
||||
submissionTime: new Date(),
|
||||
submissionTimeInMS: Date.now()
|
||||
};
|
||||
})
|
||||
});
|
||||
} else {
|
||||
@@ -254,14 +260,22 @@ export function userAttemptToDatabaseAttemptQuestionSets(
|
||||
|
||||
// If no latest question, add submission time
|
||||
if (!latestQuestion) {
|
||||
return { ...q, submissionTimeInMS: Date.now() };
|
||||
return {
|
||||
...q,
|
||||
submissionTime: new Date(),
|
||||
submissionTimeInMS: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
// If answers have changed, add submission time
|
||||
if (
|
||||
JSON.stringify(q.answers) !== JSON.stringify(latestQuestion.answers)
|
||||
) {
|
||||
return { ...q, submissionTimeInMS: Date.now() };
|
||||
return {
|
||||
...q,
|
||||
submissionTime: new Date(),
|
||||
submissionTimeInMS: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
return latestQuestion;
|
||||
@@ -810,8 +824,13 @@ export async function constructEnvExamAttempt(
|
||||
}
|
||||
|
||||
// If attempt is still in progress, return without result
|
||||
const attemptStartTimeInMS =
|
||||
attempt.startTime?.getTime() ?? attempt.startTimeInMS;
|
||||
const examTotalTimeInMS = exam.config.totalTimeInS
|
||||
? exam.config.totalTimeInS * 1000
|
||||
: exam.config.totalTimeInMS;
|
||||
const isAttemptExpired =
|
||||
attempt.startTimeInMS + exam.config.totalTimeInMS < Date.now();
|
||||
attemptStartTimeInMS + examTotalTimeInMS < Date.now();
|
||||
if (!isAttemptExpired) {
|
||||
return {
|
||||
examEnvironmentExamAttempt: {
|
||||
|
||||
Reference in New Issue
Block a user