breaking(api): remove submission time from exam env (#57365)

This commit is contained in:
Shaun Hamilton
2024-12-06 10:50:37 +02:00
committed by GitHub
parent 059b92d751
commit 9d4f63920b
6 changed files with 27 additions and 55 deletions
+3 -3
View File
@@ -45,7 +45,8 @@ export const config: EnvConfig = {
numberOfCorrectAnswers: 1,
numberOfIncorrectAnswers: 1
}
]
],
retakeTimeInMS: 24 * 60 * 60 * 1000
};
export const questionSets: EnvQuestionSet[] = [
@@ -292,8 +293,7 @@ export const examAttempt: EnvExamAttempt = {
}
],
startTimeInMS: Date.now(),
userId: defaultUserId,
submissionTimeInMS: null
userId: defaultUserId
};
export const examAttemptSansSubmissionTimeInMS: Static<
+10 -13
View File
@@ -223,15 +223,17 @@ type EnvAnswer {
/// Configuration for an exam in the Exam Environment App
type EnvConfig {
/// Human-readable exam name
name String
name String
/// Notes given about exam
note String
note String
/// Category configuration for question selection
tags EnvTagConfig[]
tags EnvTagConfig[]
/// Total time allocated for exam in milliseconds
totalTimeInMS Int
totalTimeInMS Int
/// Configuration for sets of questions
questionSets EnvQuestionSetConfig[]
questionSets EnvQuestionSetConfig[]
/// Duration after exam completion before a retake is allowed in milliseconds
retakeTimeInMS Int
}
/// Configuration for a set of questions in the Exam Environment App
@@ -267,14 +269,10 @@ model EnvExamAttempt {
/// Foreign key to generated exam id
generatedExamId String @db.ObjectId
questionSets EnvQuestionSetAttempt[]
questionSets EnvQuestionSetAttempt[]
/// Time exam was started as milliseconds since epoch
startTimeInMS Int
/// Time exam was submitted as milliseconds since epoch
///
/// As attempt might not be submitted (disconnection or quit), field is optional
submissionTimeInMS Int?
needsRetake Boolean
startTimeInMS Int
needsRetake Boolean
// Relations
user user @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -301,7 +299,6 @@ type EnvMultipleChoiceQuestionAttempt {
/// A generated exam for the Exam Environment App
///
/// This is the user-facing information for an exam.
/// TODO: Add userId?
model EnvGeneratedExam {
id String @id @default(auto()) @map("_id") @db.ObjectId
/// Foreign key to exam
@@ -435,8 +435,6 @@ describe('/exam-environment/', () => {
24 * 60 * 60 * 1000 -
mock.exam.config.totalTimeInMS -
1 * 60 * 60 * 1000;
submittedAttempt.submissionTimeInMS =
Date.now() - mock.exam.config.totalTimeInMS - 24 * 60 * 60 * 1000;
await fastifyTestInstance.prisma.envExamAttempt.create({
data: submittedAttempt
});
@@ -492,7 +490,6 @@ describe('/exam-environment/', () => {
generatedExamId: generatedExam!.id,
questionSets: [],
needsRetake: false,
submissionTimeInMS: null,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
startTimeInMS: expect.any(Number)
});
@@ -579,7 +576,8 @@ describe('/exam-environment/', () => {
config: {
name: mock.exam.config.name,
note: mock.exam.config.note,
totalTimeInMS: mock.exam.config.totalTimeInMS
totalTimeInMS: mock.exam.config.totalTimeInMS,
retakeTimeInMS: mock.exam.config.retakeTimeInMS
},
id: mock.examId
}
@@ -8,7 +8,6 @@ import * as schemas from '../schemas';
import { mapErr, syncMapErr, UpdateReqType } from '../../utils';
import { JWT_SECRET } from '../../utils/env';
import {
checkAttemptAgainstGeneratedExam,
checkPrerequisites,
constructUserExam,
userAttemptToDatabaseAttemptQuestionSets,
@@ -209,16 +208,13 @@ async function postExamGeneratedExamHandler(
: null;
if (lastAttempt) {
const attemptIsExpired =
lastAttempt.startTimeInMS + exam.config.totalTimeInMS < Date.now();
if (attemptIsExpired) {
// If exam is not submitted, use exam start time + time allocated for exam
const effectiveSubmissionTime =
lastAttempt.submissionTimeInMS ??
lastAttempt.startTimeInMS + exam.config.totalTimeInMS;
const twentyFourHoursAgo = Date.now() - 24 * 60 * 60 * 1000;
const examExpirationTime =
lastAttempt.startTimeInMS + exam.config.totalTimeInMS;
if (examExpirationTime < Date.now()) {
const retakeAllowed =
examExpirationTime + exam.config.retakeTimeInMS < Date.now();
if (effectiveSubmissionTime > twentyFourHoursAgo) {
if (!retakeAllowed) {
void reply.code(429);
// TODO: Consider sending last completed time
return reply.send(
@@ -429,20 +425,6 @@ async function postExamAttemptHandler(
latest.startTimeInMS > current.startTimeInMS ? latest : current
);
// TODO: Currently, submission time is set when all questions have been answered.
// This might not necessarily be fully submitted. So, provided there is time
// left on the clock, the attempt should still be updated, even if the submission
// time is set.
// The submission time just needs to be updated.
// if (latestAttempt.submissionTimeInMS !== null) {
// void reply.code(403);
// return reply.send(
// ERRORS.FCC_EINVAL_EXAM_ENVIRONMENT_EXAM_ATTEMPT(
// 'Attempt has already been submitted.'
// )
// );
// }
const maybeExam = await mapErr(
this.prisma.envExam.findUnique({
where: {
@@ -515,12 +497,6 @@ async function postExamAttemptHandler(
validateAttempt(generatedExam, databaseAttemptQuestionSets)
);
// If all questions have been answered, add submission time
const allQuestionsAnswered = checkAttemptAgainstGeneratedExam(
databaseAttemptQuestionSets,
generatedExam
);
// Update attempt in database
const maybeUpdatedAttempt = await mapErr(
this.prisma.envExamAttempt.update({
@@ -528,8 +504,6 @@ async function postExamAttemptHandler(
id: latestAttempt.id
},
data: {
// NOTE: submission time is set to null, because it just depends on whether all questions have been answered.
submissionTimeInMS: allQuestionsAnswered ? Date.now() : null,
questionSets: databaseAttemptQuestionSets,
// If attempt is not valid, immediately flag attempt as needing retake
// TODO: If `needsRetake`, prevent further submissions?
@@ -592,7 +566,8 @@ async function getExams(
config: {
name: exam.config.name,
note: exam.config.note,
totalTimeInMS: exam.config.totalTimeInMS
totalTimeInMS: exam.config.totalTimeInMS,
retakeTimeInMS: exam.config.retakeTimeInMS
},
canTake: isExamPrerequisitesMet
};
+2 -1
View File
@@ -12,7 +12,8 @@ export const examEnvironmentExams = {
config: Type.Object({
name: Type.String(),
note: Type.String(),
totalTimeInMS: Type.Number()
totalTimeInMS: Type.Number(),
retakeTimeInMS: Type.Number()
}),
canTake: Type.Boolean()
})
+2 -1
View File
@@ -101,7 +101,8 @@ export function constructUserExam(
const config = {
totalTimeInMS: exam.config.totalTimeInMS,
name: exam.config.name,
note: exam.config.note
note: exam.config.note,
retakeTimeInMS: exam.config.retakeTimeInMS
};
const userExam: UserExam = {