feat: remove deprecated exam fields (#62852)

This commit is contained in:
Shaun Hamilton
2025-10-16 18:54:58 +02:00
committed by GitHub
parent 5add4262bd
commit 59cab66cf2
11 changed files with 43 additions and 128 deletions

View File

@@ -18,7 +18,6 @@ export const oid = () => new ObjectId().toString();
export const examId = oid();
export const config = {
totalTimeInMS: 2 * 60 * 60 * 1000,
totalTimeInS: 2 * 60 * 60,
tags: [],
name: 'Test Exam',
@@ -47,7 +46,6 @@ export const config = {
numberOfIncorrectAnswers: 1
}
],
retakeTimeInMS: 24 * 60 * 60 * 1000,
retakeTimeInS: 24 * 60 * 60
} satisfies ExamEnvironmentConfig;
@@ -264,7 +262,6 @@ export const examAttempt: ExamEnvironmentExamAttempt = {
{
id: generatedExam.questionSets[0]!.questions[0]!.id,
answers: [generatedExam.questionSets[0]!.questions[0]!.answers[0]!],
submissionTimeInMS: Date.now(),
submissionTime: new Date()
}
]
@@ -275,7 +272,6 @@ export const examAttempt: ExamEnvironmentExamAttempt = {
{
id: generatedExam.questionSets[1]!.questions[0]!.id,
answers: [generatedExam.questionSets[1]!.questions[0]!.answers[1]!],
submissionTimeInMS: Date.now(),
submissionTime: new Date()
}
]
@@ -286,19 +282,16 @@ export const examAttempt: ExamEnvironmentExamAttempt = {
{
id: generatedExam.questionSets[2]!.questions[0]!.id,
answers: [generatedExam.questionSets[2]!.questions[0]!.answers[1]!],
submissionTimeInMS: Date.now(),
submissionTime: new Date()
},
{
id: generatedExam.questionSets[2]!.questions[1]!.id,
answers: [generatedExam.questionSets[2]!.questions[1]!.answers[0]!],
submissionTimeInMS: Date.now(),
submissionTime: new Date()
}
]
}
],
startTimeInMS: Date.now(),
startTime: new Date(),
userId: defaultUserId,
version: 2

View File

@@ -14,8 +14,7 @@ model ExamCreatorExam {
deprecated Boolean
/// Version of the record
/// The default must be incremented by 1, if anything in the schema changes
version Int @default(2)
version Int @default(3)
}
/// Exam Creator application collection to store authZ users.
@@ -30,10 +29,8 @@ model ExamCreatorUser {
github_id Int?
name String
picture String?
/// TODO: After migration, remove optionality
settings ExamCreatorUserSettings?
version Int @default(1)
settings ExamCreatorUserSettings
version Int @default(2)
ExamCreatorSession ExamCreatorSession[]
}
@@ -55,8 +52,7 @@ model ExamCreatorSession {
/// Expiration date for record.
expires_at DateTime
version Int @default(1)
version Int @default(1)
ExamCreatorUser ExamCreatorUser @relation(fields: [user_id], references: [id])
}

View File

@@ -12,7 +12,7 @@ model ExamEnvironmentExam {
deprecated Boolean
/// Version of the record
/// The default must be incremented by 1, if anything in the schema changes
version Int @default(2)
version Int @default(3)
// Relations
generatedExams ExamEnvironmentGeneratedExam[]
@@ -83,16 +83,12 @@ type ExamEnvironmentConfig {
note String
/// Category configuration for question selection
tags ExamEnvironmentTagConfig[]
/// Deprecated: use `totalTimeInS` instead
totalTimeInMS Int
/// Total time allocated for exam in seconds
totalTimeInS Int?
totalTimeInS Int
/// Configuration for sets of questions
questionSets ExamEnvironmentQuestionSetConfig[]
/// Deprecated: use `retakeTimeInS` instead
retakeTimeInMS Int
/// Duration after exam completion before a retake is allowed in seconds
retakeTimeInS Int?
retakeTimeInS Int
/// Passing percent for the exam
passingPercent Float
}
@@ -130,14 +126,12 @@ model ExamEnvironmentExamAttempt {
/// Foreign key to generated exam id
generatedExamId String @db.ObjectId
questionSets ExamEnvironmentQuestionSetAttempt[]
/// Deprecated: Use `startTime` instead
startTimeInMS Int
questionSets ExamEnvironmentQuestionSetAttempt[]
/// Time exam was started
startTime DateTime?
startTime DateTime
/// Version of the record
/// The default must be incremented by 1, if anything in the schema changes
version Int @default(2)
version Int @default(3)
// Relations
user user @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -153,15 +147,13 @@ type ExamEnvironmentQuestionSetAttempt {
type ExamEnvironmentMultipleChoiceQuestionAttempt {
/// Foreign key to question
id String @db.ObjectId
id String @db.ObjectId
/// An array of foreign keys to answers
answers String[] @db.ObjectId
/// Deprecated: Use `submissionTime` instead
submissionTimeInMS Int
answers String[] @db.ObjectId
/// Time answers to question were submitted
///
/// If the question is later revisited, this field is updated
submissionTime DateTime?
submissionTime DateTime
}
/// A generated exam for the Exam Environment App

View File

@@ -102,7 +102,6 @@ describe('/exam-environment/', () => {
data: {
examId,
generatedExamId: mock.oid(),
startTimeInMS: Date.now(),
startTime: new Date(),
userId: defaultUserId
}
@@ -134,7 +133,6 @@ describe('/exam-environment/', () => {
data: {
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
}
@@ -166,7 +164,6 @@ describe('/exam-environment/', () => {
data: {
examId: mock.examId,
generatedExamId: mock.oid(),
startTimeInMS: Date.now(),
startTime: new Date(),
userId: defaultUserId
}
@@ -238,7 +235,6 @@ describe('/exam-environment/', () => {
userId: defaultUserId,
examId: mock.examId,
generatedExamId: mock.generatedExam.id,
startTimeInMS: Date.now(),
startTime: new Date(),
questionSets: []
}
@@ -342,7 +338,6 @@ describe('/exam-environment/', () => {
const recentExamAttempt = {
...mock.examAttempt,
// Set start time such that exam has just expired
startTimeInMS: Date.now() - examTotalTimeInMS,
startTime: new Date(Date.now() - examTotalTimeInMS)
};
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({
@@ -375,8 +370,6 @@ describe('/exam-environment/', () => {
},
data: {
// Set start time such that exam has expired, but retake time -1s has passed
startTimeInMS:
Date.now() - (examTotalTimeInMS + (examRetakeTimeInMS - 1000)),
startTime: new Date(
Date.now() - (examTotalTimeInMS + (examRetakeTimeInMS - 1000))
)
@@ -408,8 +401,6 @@ describe('/exam-environment/', () => {
// 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() - examTotalTimeInMS + (24 * 60 * 60 * 1000 + 1000);
recentExamAttempt.startTime = new Date(
Date.now() - (examTotalTimeInMS + (24 * 60 * 60 * 1000 + 1000))
);
@@ -501,8 +492,8 @@ describe('/exam-environment/', () => {
data: {
startTime: new Date(
Date.now() -
mock.exam.config.totalTimeInMS -
mock.exam.config.retakeTimeInMS
mock.exam.config.totalTimeInS * 1000 -
mock.exam.config.retakeTimeInS * 1000
)
}
});
@@ -537,8 +528,8 @@ describe('/exam-environment/', () => {
data: {
startTime: new Date(
Date.now() -
mock.exam.config.totalTimeInMS -
mock.exam.config.retakeTimeInMS
mock.exam.config.totalTimeInS * 1000 -
mock.exam.config.retakeTimeInS * 1000
)
}
});
@@ -603,8 +594,6 @@ describe('/exam-environment/', () => {
// 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)
});
});
@@ -721,9 +710,7 @@ describe('/exam-environment/', () => {
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
retakeTimeInS: mock.exam.config.retakeTimeInS
},
id: mock.examId
}
@@ -798,9 +785,7 @@ describe('/exam-environment/', () => {
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
retakeTimeInS: mock.exam.config.retakeTimeInS
},
id: mock.examId
}
@@ -816,7 +801,6 @@ describe('/exam-environment/', () => {
const recentExamAttempt = {
...mock.examAttempt,
userId: defaultUserId,
startTimeInMS: Date.now() - examTotalTimeInMS,
startTime: new Date(Date.now() - examTotalTimeInMS)
};
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.create({
@@ -837,8 +821,6 @@ describe('/exam-environment/', () => {
await fastifyTestInstance.prisma.examEnvironmentExamAttempt.update({
where: { id: recentExamAttempt.id },
data: {
startTimeInMS:
Date.now() - (examTotalTimeInMS + examRetakeTimeInMS + 1000),
startTime: new Date(
Date.now() - (examTotalTimeInMS + examRetakeTimeInMS + 1000)
)
@@ -862,8 +844,6 @@ describe('/exam-environment/', () => {
const examAttempt = {
...mock.examAttempt,
userId: defaultUserId,
startTimeInMS:
Date.now() - (examTotalTimeInMS + examRetakeTimeInMS + 1000),
startTime: new Date(
Date.now() - (examTotalTimeInMS + examRetakeTimeInMS + 1000)
)
@@ -962,7 +942,6 @@ describe('/exam-environment/', () => {
examId: mock.exam.id,
result: null,
startTime: attempt.startTime,
startTimeInMS: attempt.startTimeInMS,
questionSets: attempt.questionSets
};
@@ -1003,7 +982,6 @@ describe('/exam-environment/', () => {
examId: mock.exam.id,
result: null,
startTime: attempt.startTime,
startTimeInMS: attempt.startTimeInMS,
questionSets: attempt.questionSets
};
@@ -1015,7 +993,6 @@ describe('/exam-environment/', () => {
const examAttempt = structuredClone(mock.examAttempt);
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({
@@ -1044,7 +1021,6 @@ describe('/exam-environment/', () => {
passingPercent: 80
},
startTime: attempt.startTime,
startTimeInMS: attempt.startTimeInMS,
questionSets: attempt.questionSets
};
@@ -1100,7 +1076,6 @@ describe('/exam-environment/', () => {
examId: mock.exam.id,
result: null,
startTime: attempt.startTime,
startTimeInMS: attempt.startTimeInMS,
questionSets: attempt.questionSets
};
@@ -1131,7 +1106,6 @@ describe('/exam-environment/', () => {
examId: mock.exam.id,
result: null,
startTime: attempt.startTime,
startTimeInMS: attempt.startTimeInMS,
questionSets: attempt.questionSets
};
@@ -1143,7 +1117,6 @@ describe('/exam-environment/', () => {
const examAttempt = structuredClone(mock.examAttempt);
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({
@@ -1170,7 +1143,6 @@ describe('/exam-environment/', () => {
passingPercent: 80
},
startTime: attempt.startTime,
startTimeInMS: attempt.startTimeInMS,
questionSets: attempt.questionSets
};
@@ -1221,7 +1193,6 @@ describe('/exam-environment/', () => {
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)

View File

@@ -260,10 +260,8 @@ async function postExamGeneratedExamHandler(
const lastAttempt = examAttempts.length
? examAttempts.reduce((latest, current) => {
const latestStartTime =
latest.startTime?.getTime() ?? latest.startTimeInMS;
const currentStartTime =
current.startTime?.getTime() ?? current.startTimeInMS;
const latestStartTime = latest.startTime;
const currentStartTime = current.startTime;
return latestStartTime > currentStartTime ? latest : current;
})
: null;
@@ -304,17 +302,12 @@ async function postExamGeneratedExamHandler(
);
}
const lastAttemptStartTime =
lastAttempt.startTime?.getTime() ?? lastAttempt.startTimeInMS;
const examTotalTimeInMS = exam.config.totalTimeInS
? exam.config.totalTimeInS * 1000
: exam.config.totalTimeInMS;
const lastAttemptStartTime = lastAttempt.startTime.getTime();
const examTotalTimeInMS = exam.config.totalTimeInS * 1000;
const examExpirationTime = lastAttemptStartTime + examTotalTimeInMS;
if (examExpirationTime < Date.now()) {
const examRetakeTimeInMS = exam.config.retakeTimeInS
? exam.config.retakeTimeInS * 1000
: exam.config.retakeTimeInMS;
const examRetakeTimeInMS = exam.config.retakeTimeInS * 1000;
const retakeAllowed =
examExpirationTime + examRetakeTimeInMS < Date.now();
@@ -466,7 +459,6 @@ async function postExamGeneratedExamHandler(
userId: user.id,
examId: exam.id,
generatedExamId: generatedExam.id,
startTimeInMS: Date.now(),
startTime: new Date(),
questionSets: []
}
@@ -565,9 +557,8 @@ async function postExamAttemptHandler(
}
const latestAttempt = attempts.reduce((latest, current) => {
const latestStartTime = latest.startTime?.getTime() ?? latest.startTimeInMS;
const currentStartTime =
current.startTime?.getTime() ?? current.startTimeInMS;
const latestStartTime = latest.startTime;
const currentStartTime = current.startTime;
return latestStartTime > currentStartTime ? latest : current;
});
@@ -600,11 +591,8 @@ async function postExamAttemptHandler(
);
}
const latestAttemptStartTime =
latestAttempt.startTime?.getTime() ?? latestAttempt.startTimeInMS;
const examTotalTimeInMS = exam.config.totalTimeInS
? exam.config.totalTimeInS * 1000
: exam.config.totalTimeInMS;
const latestAttemptStartTime = latestAttempt.startTime.getTime();
const examTotalTimeInMS = exam.config.totalTimeInS * 1000;
const isAttemptExpired =
latestAttemptStartTime + examTotalTimeInMS < Date.now();
@@ -747,7 +735,6 @@ async function getExams(
select: {
id: true,
examId: true,
startTimeInMS: true,
startTime: true
}
})
@@ -772,9 +759,7 @@ async function getExams(
config: {
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
},
@@ -798,10 +783,8 @@ async function getExams(
const lastAttempt = attemptsForExam.length
? attemptsForExam.reduce((latest, current) => {
const latestStartTime =
latest.startTime?.getTime() ?? latest.startTimeInMS;
const currentStartTime =
current.startTime?.getTime() ?? current.startTimeInMS;
const latestStartTime = latest.startTime;
const currentStartTime = current.startTime;
return latestStartTime > currentStartTime ? latest : current;
})
: null;
@@ -813,14 +796,9 @@ 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 lastAttemptStartTime = lastAttempt.startTime.getTime();
const examTotalTimeInMS = exam.config.totalTimeInS * 1000;
const examRetakeTimeInMS = exam.config.retakeTimeInS * 1000;
const retakeDateInMS =
lastAttemptStartTime + examTotalTimeInMS + examRetakeTimeInMS;

View File

@@ -29,7 +29,6 @@ export const examEnvironmentPostExamAttempt = {
const examEnvAttempt = Type.Object({
id: Type.String(),
examId: Type.String(),
startTimeInMS: Type.Number(),
startTime: Type.String({ format: 'date-time' }),
questionSets: Type.Array(
Type.Object({
@@ -38,7 +37,6 @@ const examEnvAttempt = Type.Object({
Type.Object({
id: Type.String(),
answers: Type.Array(Type.String()),
submissionTimeInMS: Type.Number(),
submissionTime: Type.String({ format: 'date-time' })
})
)

View File

@@ -11,9 +11,7 @@ export const examEnvironmentExams = {
config: Type.Object({
name: Type.String(),
note: Type.String(),
totalTimeInMS: Type.Number(),
totalTimeInS: Type.Number(),
retakeTimeInMS: Type.Number(),
retakeTimeInS: Type.Number(),
passingPercent: Type.Number()
}),

View File

@@ -446,10 +446,8 @@ describe('Exam Environment Schema', () => {
note: '',
passingPercent: 0.0,
questionSets: configQuestionSets,
retakeTimeInMS: 0,
retakeTimeInS: 0,
tags,
totalTimeInMS: 0,
totalTimeInS: 0
};
@@ -527,13 +525,11 @@ describe('Exam Environment Schema', () => {
{
answers: [oid()],
id: oid(),
submissionTime: new Date(),
submissionTimeInMS: Date.now()
submissionTime: new Date()
}
]
}
],
startTimeInMS: Date.now(),
startTime: new Date(),
userId: oid()
}

View File

@@ -111,11 +111,9 @@ 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
};
@@ -245,8 +243,7 @@ export function userAttemptToDatabaseAttemptQuestionSets(
questions: questionSet.questions.map(q => {
return {
...q,
submissionTime: new Date(),
submissionTimeInMS: Date.now()
submissionTime: new Date()
};
})
});
@@ -262,8 +259,7 @@ export function userAttemptToDatabaseAttemptQuestionSets(
if (!latestQuestion) {
return {
...q,
submissionTime: new Date(),
submissionTimeInMS: Date.now()
submissionTime: new Date()
};
}
@@ -273,8 +269,7 @@ export function userAttemptToDatabaseAttemptQuestionSets(
) {
return {
...q,
submissionTime: new Date(),
submissionTimeInMS: Date.now()
submissionTime: new Date()
};
}
@@ -824,11 +819,8 @@ 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 attemptStartTimeInMS = attempt.startTime.getTime();
const examTotalTimeInMS = exam.config.totalTimeInS * 1000;
const isAttemptExpired =
attemptStartTimeInMS + examTotalTimeInMS < Date.now();
if (!isAttemptExpired) {

View File

@@ -59,8 +59,8 @@ export function Attempts({ id }: AttemptsProps) {
</thead>
<tbody>
{attempts.map(attempt => (
<tr key={attempt.startTimeInMS}>
<td>{new Date(attempt.startTimeInMS).toTimeString()}</td>
<tr key={attempt.startTime}>
<td>{new Date(attempt.startTime).toTimeString()}</td>
<td>
{attempt.result
? `${attempt.result.percent}%`

View File

@@ -223,7 +223,8 @@ export interface Exam {
export interface Attempt {
id: string;
examId: string;
startTimeInMS: number;
// ISO 8601 string
startTime: string;
questionSets: unknown[];
result?: {
passed: boolean;