Skip to content

Commit be55546

Browse files
authored
Merge pull request #106 from Ibinola/feat/add-quiz-timer
feat(quiz): enforce time limit and max attempt rules on quiz submission closes #96
2 parents b8145dc + 0ecb151 commit be55546

2 files changed

Lines changed: 116 additions & 13 deletions

File tree

backend/src/quizzes/entities/quiz.entity.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ export class Quiz {
3232
@Column()
3333
passingScore: number;
3434

35-
@Column()
36-
timeLimit: number;
37-
38-
@Column()
35+
@Column({ type: 'int', default: 1 })
3936
maxAttempts: number;
4037

38+
@Column({ type: 'int', nullable: true })
39+
timeLimit: number;
40+
4141
@OneToMany(() => QuizQuestion, (quizQuestion) => quizQuestion.quiz)
4242
questions: QuizQuestion[];
4343

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,129 @@
1-
import { Injectable } from '@nestjs/common';
1+
import {
2+
Injectable,
3+
ForbiddenException,
4+
NotFoundException,
5+
} from '@nestjs/common';
6+
import { InjectRepository } from '@nestjs/typeorm';
7+
import { Repository } from 'typeorm';
8+
import { Quiz } from './entities/quiz.entity';
29
import { CreateQuizDto } from './dto/create-quiz.dto';
310
import { UpdateQuizDto } from './dto/update-quiz.dto';
11+
import { SubmitQuizAttemptDto } from './dto/submit-quiz-attempt.dto';
12+
import { QuizAttempt } from './entities/quiz-attempt.entity';
413

514
@Injectable()
615
export class QuizzesService {
16+
constructor(
17+
@InjectRepository(Quiz)
18+
private readonly quizRepo: Repository<Quiz>,
19+
20+
@InjectRepository(QuizAttempt)
21+
private readonly submissionRepo: Repository<QuizAttempt>,
22+
) {}
23+
24+
// Create a new quiz
725
create(createQuizDto: CreateQuizDto) {
8-
return 'This action adds a new quiz';
26+
const quiz = this.quizRepo.create(createQuizDto);
27+
return this.quizRepo.save(quiz);
928
}
1029

30+
// Get all quizzes
1131
findAll() {
12-
return `This action returns all quizzes`;
32+
return this.quizRepo.find();
33+
}
34+
35+
// Get a single quiz
36+
async findOne(id: number) {
37+
const quiz = await this.quizRepo.findOne({ where: { id } });
38+
if (!quiz) throw new NotFoundException('Quiz not found');
39+
return quiz;
40+
}
41+
42+
// Update a quiz
43+
async update(id: number, updateQuizDto: UpdateQuizDto) {
44+
await this.quizRepo.update(id, updateQuizDto);
45+
return this.findOne(id);
46+
}
47+
48+
// Delete a quiz
49+
async remove(id: number) {
50+
await this.quizRepo.delete(id);
51+
return { message: 'Quiz deleted' };
1352
}
1453

15-
findOne(id: number) {
16-
return `This action returns a #${id} quiz`;
54+
// Validate user's quiz submission
55+
async validateQuizSubmission(
56+
userId: string,
57+
quizId: string | number,
58+
submissionId?: string,
59+
) {
60+
const quiz = await this.quizRepo.findOne({ where: { id: Number(quizId) } });
61+
62+
if (!quiz) throw new NotFoundException('Quiz not found');
63+
64+
const attempts = await this.submissionRepo.count({
65+
where: { userId: String(userId), quizId: String(quizId) },
66+
});
67+
if (attempts >= quiz.maxAttempts) {
68+
throw new ForbiddenException('Max attempts exceeded');
69+
}
70+
71+
if (submissionId) {
72+
const submission = await this.submissionRepo.findOne({
73+
where: { id: submissionId },
74+
});
75+
if (!submission) throw new NotFoundException('Submission not found');
76+
77+
const elapsed =
78+
(new Date().getTime() - new Date(submission.startTime).getTime()) /
79+
1000;
80+
if (quiz.timeLimit && elapsed > quiz.timeLimit) {
81+
throw new ForbiddenException('Time limit exceeded');
82+
}
83+
}
1784
}
1885

19-
update(id: number, updateQuizDto: UpdateQuizDto) {
20-
return `This action updates a #${id} quiz`;
86+
// Handle quiz submission
87+
async submitQuizAttempt(submissionId: string, dto: SubmitQuizAttemptDto) {
88+
const submission = await this.submissionRepo.findOne({
89+
where: { id: submissionId },
90+
relations: ['quiz'],
91+
});
92+
93+
if (!submission) throw new NotFoundException('Submission not found');
94+
95+
const now = new Date();
96+
const elapsed =
97+
(now.getTime() - new Date(submission.startTime).getTime()) / 1000;
98+
99+
if (submission.quiz.timeLimit && elapsed > submission.quiz.timeLimit) {
100+
throw new ForbiddenException('Time limit exceeded');
101+
}
102+
103+
submission.answers = dto.answers;
104+
submission.endTime = now;
105+
106+
return this.submissionRepo.save(submission);
21107
}
22108

23-
remove(id: number) {
24-
return `This action removes a #${id} quiz`;
109+
async startQuizAttempt(userId: string, quizId: string) {
110+
const quiz = await this.quizRepo.findOne({ where: { id: Number(quizId) } });
111+
if (!quiz) throw new NotFoundException('Quiz not found');
112+
113+
const attempts = await this.submissionRepo.count({
114+
where: { userId, quizId },
115+
});
116+
if (attempts >= quiz.maxAttempts) {
117+
throw new ForbiddenException('Max attempts exceeded');
118+
}
119+
120+
const submission = this.submissionRepo.create({
121+
userId,
122+
quizId,
123+
attemptNumber: attempts + 1,
124+
startTime: new Date(),
125+
});
126+
127+
return this.submissionRepo.save(submission);
25128
}
26129
}

0 commit comments

Comments
 (0)