Skip to content

Commit ff7bb7d

Browse files
committed
[Feat] Implement migration for existing quiz attempts and enhance statistics endpoints
1 parent e248916 commit ff7bb7d

12 files changed

Lines changed: 1063 additions & 238 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
const sequelize = require('./src/config/database');
2+
const { QuizProgress } = require('./src/models');
3+
4+
async function migrateExistingQuizAttempts() {
5+
try {
6+
console.log('[LOG migration] ========= Starting migration of existing quiz attempts');
7+
8+
// Get all quiz attempts that don't have corresponding quiz progress records
9+
const existingAttempts = await sequelize.query(`
10+
SELECT
11+
qa.user_id,
12+
qa.quiz_id,
13+
MAX(qa.score) as best_score,
14+
COUNT(*) as attempts_count,
15+
MAX(qa.completed_at) as last_attempt_date
16+
FROM quiz_attempts qa
17+
LEFT JOIN quiz_progress qp ON qa.user_id = qp.user_id AND qa.quiz_id = qp.quiz_id
18+
WHERE qp.id IS NULL
19+
GROUP BY qa.user_id, qa.quiz_id
20+
`, {
21+
type: sequelize.QueryTypes.SELECT
22+
});
23+
24+
console.log(`[LOG migration] ========= Found ${existingAttempts.length} quiz attempts without progress records`);
25+
26+
// Create quiz progress records for each
27+
for (const attempt of existingAttempts) {
28+
try {
29+
const progress = attempt.best_score >= 70 ? 100 : attempt.best_score;
30+
31+
await QuizProgress.create({
32+
user_id: attempt.user_id,
33+
quiz_id: attempt.quiz_id,
34+
progress,
35+
best_score: attempt.best_score,
36+
attempts_count: attempt.attempts_count,
37+
last_attempt_date: attempt.last_attempt_date
38+
});
39+
40+
console.log(`[LOG migration] ========= Created progress record for user ${attempt.user_id}, quiz ${attempt.quiz_id}`);
41+
} catch (error) {
42+
console.error(`[LOG migration] ========= Error creating progress for user ${attempt.user_id}, quiz ${attempt.quiz_id}:`, error.message);
43+
}
44+
}
45+
46+
console.log('[LOG migration] ========= Migration completed successfully');
47+
48+
// Verify the results
49+
const totalProgressRecords = await QuizProgress.count();
50+
console.log(`[LOG migration] ========= Total quiz progress records now: ${totalProgressRecords}`);
51+
52+
} catch (error) {
53+
console.error('[LOG migration] ========= Migration failed:', error);
54+
} finally {
55+
await sequelize.close();
56+
}
57+
}
58+
59+
// Run the migration
60+
if (require.main === module) {
61+
migrateExistingQuizAttempts();
62+
}
63+
64+
module.exports = { migrateExistingQuizAttempts };

Backend/src/controllers/quiz.controller.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,52 @@ const submitQuizAttempt = async (req, res) => {
425425
}
426426
}
427427

428+
// Update quiz progress statistics
429+
try {
430+
const { QuizProgress } = require('../models');
431+
432+
// Find or create quiz progress
433+
let quizProgress = await QuizProgress.findOne({
434+
where: {
435+
user_id: req.user.id,
436+
quiz_id: quizId
437+
}
438+
});
439+
440+
if (quizProgress) {
441+
// Update existing record
442+
quizProgress.attempts_count += 1;
443+
quizProgress.last_attempt_date = new Date();
444+
445+
// Update best score if the new score is higher
446+
if (quizProgress.best_score === null || scorePercentage > quizProgress.best_score) {
447+
quizProgress.best_score = scorePercentage;
448+
}
449+
450+
// Update progress (consider quiz completed if score >= 70%)
451+
const newProgress = scorePercentage >= 70 ? 100 : Math.max(quizProgress.progress, scorePercentage);
452+
quizProgress.progress = newProgress;
453+
454+
await quizProgress.save();
455+
console.log('[LOG quiz] ========= Updated quiz progress for user:', req.user.id, 'quiz:', quizId);
456+
} else {
457+
// Create new record
458+
const progress = scorePercentage >= 70 ? 100 : scorePercentage;
459+
quizProgress = await QuizProgress.create({
460+
user_id: req.user.id,
461+
quiz_id: quizId,
462+
progress,
463+
best_score: scorePercentage,
464+
attempts_count: 1,
465+
last_attempt_date: new Date()
466+
});
467+
console.log('[LOG quiz] ========= Created new quiz progress for user:', req.user.id, 'quiz:', quizId);
468+
}
469+
} catch (progressError) {
470+
console.error('[LOG quiz] ========= Error updating quiz progress:', progressError);
471+
// Don't fail the main request if progress update fails
472+
}
473+
428474
res.json({
429475
message: 'Quiz attempt submitted successfully',
430476
attemptId,

Backend/src/controllers/statistics.controller.js

Lines changed: 212 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,12 @@ const getUserStatistics = async (req, res) => {
5555
type: sequelize.QueryTypes.SELECT
5656
});
5757

58-
// Get study hours for the last 7 days
59-
const oneWeekAgo = new Date();
60-
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
61-
58+
// Get all study hours (all-time statistics)
6259
const studySessions = await StudySession.findAll({
6360
where: {
64-
user_id: userId,
65-
study_date: {
66-
[Op.gte]: oneWeekAgo
67-
}
61+
user_id: userId
6862
},
69-
order: [['study_date', 'ASC']]
63+
order: [['study_date', 'DESC']]
7064
});
7165

7266
// Get aggregate statistics
@@ -162,6 +156,212 @@ const getUserStatistics = async (req, res) => {
162156
}
163157
};
164158

159+
/**
160+
* Get quiz performance statistics only
161+
*/
162+
const getQuizPerformance = async (req, res) => {
163+
try {
164+
const userId = req.user.id;
165+
166+
console.log('[LOG statistics] ========= Fetching quiz performance for user:', userId);
167+
168+
// Get quiz progress statistics
169+
const quizProgress = await QuizProgress.findAll({
170+
where: { user_id: userId },
171+
include: [
172+
{
173+
model: Quiz,
174+
as: 'quiz',
175+
attributes: ['title']
176+
}
177+
],
178+
order: [['updated_at', 'DESC']]
179+
});
180+
181+
// Get quiz aggregate statistics
182+
const [aggregateStats] = await sequelize.query(`
183+
SELECT
184+
COUNT(DISTINCT qp.quiz_id) as total_quizzes_attempted,
185+
COALESCE(AVG(qp.progress), 0) as avg_quiz_progress,
186+
COALESCE(AVG(qp.best_score), 0) as avg_quiz_score,
187+
COALESCE(MAX(qp.best_score), 0) as highest_score,
188+
COALESCE(MIN(qp.best_score), 0) as lowest_score
189+
FROM quiz_progress qp
190+
WHERE qp.user_id = ?
191+
`, {
192+
replacements: [userId],
193+
type: sequelize.QueryTypes.SELECT
194+
});
195+
196+
const formattedQuizProgress = quizProgress.map(qp => ({
197+
quizId: qp.quiz_id,
198+
quizTitle: qp.quiz?.title || 'Unknown Quiz',
199+
progress: qp.progress,
200+
bestScore: qp.best_score,
201+
attemptsCount: qp.attempts_count,
202+
updatedAt: qp.updated_at
203+
}));
204+
205+
res.json({
206+
quiz_progress: formattedQuizProgress,
207+
summary: {
208+
total_quizzes_attempted: aggregateStats.total_quizzes_attempted || 0,
209+
avg_quiz_progress: parseFloat(aggregateStats.avg_quiz_progress || 0).toFixed(2),
210+
avg_quiz_score: parseFloat(aggregateStats.avg_quiz_score || 0).toFixed(2),
211+
highest_score: aggregateStats.highest_score || 0,
212+
lowest_score: aggregateStats.lowest_score || 0
213+
}
214+
});
215+
} catch (error) {
216+
console.error('[LOG statistics] ========= Error fetching quiz performance:', error);
217+
res.status(500).json({
218+
message: 'Error fetching quiz performance',
219+
error: error.message
220+
});
221+
}
222+
};
223+
224+
/**
225+
* Get note progress statistics only
226+
*/
227+
const getNoteProgress = async (req, res) => {
228+
try {
229+
const userId = req.user.id;
230+
231+
console.log('[LOG statistics] ========= Fetching note progress for user:', userId);
232+
233+
// Get note progress statistics
234+
const noteProgress = await sequelize.query(`
235+
SELECT np.*, n.title
236+
FROM note_progress np
237+
JOIN notes n ON np.note_id = n.id
238+
WHERE np.user_id = ?
239+
ORDER BY np.updated_at DESC
240+
`, {
241+
replacements: [userId],
242+
type: sequelize.QueryTypes.SELECT
243+
});
244+
245+
// Get note aggregate statistics
246+
const [aggregateStats] = await sequelize.query(`
247+
SELECT
248+
COUNT(DISTINCT np.note_id) as total_notes_read,
249+
COALESCE(AVG(np.progress), 0) as avg_note_progress
250+
FROM note_progress np
251+
WHERE np.user_id = ?
252+
`, {
253+
replacements: [userId],
254+
type: sequelize.QueryTypes.SELECT
255+
});
256+
257+
const formattedNoteProgress = noteProgress && Array.isArray(noteProgress) ?
258+
noteProgress.map(np => ({
259+
noteId: np.note_id,
260+
noteTitle: np.title || 'Unknown Note',
261+
progress: np.progress,
262+
updatedAt: np.updated_at
263+
})) : [];
264+
265+
res.json({
266+
note_progress: formattedNoteProgress,
267+
summary: {
268+
total_notes_read: aggregateStats.total_notes_read || 0,
269+
avg_note_progress: parseFloat(aggregateStats.avg_note_progress || 0).toFixed(2)
270+
}
271+
});
272+
} catch (error) {
273+
console.error('[LOG statistics] ========= Error fetching note progress:', error);
274+
res.status(500).json({
275+
message: 'Error fetching note progress',
276+
error: error.message
277+
});
278+
}
279+
};
280+
281+
/**
282+
* Get study sessions only
283+
*/
284+
const getStudySessions = async (req, res) => {
285+
try {
286+
const userId = req.user.id;
287+
const { days } = req.query; // Optional days filter
288+
289+
console.log('[LOG statistics] ========= Fetching study sessions for user:', userId);
290+
291+
let whereClause = { user_id: userId };
292+
293+
// Only apply date filter if days parameter is provided
294+
if (days && parseInt(days) > 0) {
295+
const startDate = new Date();
296+
startDate.setDate(startDate.getDate() - parseInt(days));
297+
whereClause.study_date = { [Op.gte]: startDate };
298+
}
299+
300+
const studySessions = await StudySession.findAll({
301+
where: whereClause,
302+
order: [['study_date', 'DESC']]
303+
});
304+
305+
// Get study session aggregate statistics
306+
let aggregateQuery = `
307+
SELECT
308+
COALESCE(SUM(ss.hours), 0) as total_study_hours,
309+
COALESCE(AVG(ss.hours), 0) as avg_daily_hours,
310+
COALESCE(AVG(ss.productivity_score), 0) as avg_productivity_score,
311+
COUNT(*) as total_sessions
312+
FROM study_sessions ss
313+
WHERE ss.user_id = ?
314+
`;
315+
316+
let replacements = [userId];
317+
318+
// Add date filter if days parameter is provided
319+
if (days && parseInt(days) > 0) {
320+
const startDate = new Date();
321+
startDate.setDate(startDate.getDate() - parseInt(days));
322+
aggregateQuery += ` AND ss.study_date >= ?`;
323+
replacements.push(startDate);
324+
}
325+
326+
const [aggregateStats] = await sequelize.query(aggregateQuery, {
327+
replacements,
328+
type: sequelize.QueryTypes.SELECT
329+
});
330+
331+
const formattedStudySessions = studySessions.map(ss => {
332+
// Calculate productivity change (simplified)
333+
const productivityChange = ss.productivity_score ?
334+
(ss.productivity_score - (ss.previousDayScore || ss.productivity_score)) :
335+
0;
336+
337+
return {
338+
date: ss.study_date,
339+
hours: ss.hours,
340+
productivityScore: ss.productivity_score || 0,
341+
productivityChange,
342+
notes: ss.notes,
343+
updatedAt: ss.updated_at
344+
};
345+
});
346+
347+
res.json({
348+
study_hours: formattedStudySessions,
349+
summary: {
350+
total_study_hours: aggregateStats.total_study_hours || 0,
351+
avg_daily_hours: parseFloat(aggregateStats.avg_daily_hours || 0).toFixed(2),
352+
avg_productivity_score: parseFloat(aggregateStats.avg_productivity_score || 0).toFixed(2),
353+
total_sessions: aggregateStats.total_sessions || 0
354+
}
355+
});
356+
} catch (error) {
357+
console.error('[LOG statistics] ========= Error fetching study sessions:', error);
358+
res.status(500).json({
359+
message: 'Error fetching study sessions',
360+
error: error.message
361+
});
362+
}
363+
};
364+
165365
/**
166366
* Update topic progress
167367
*/
@@ -461,6 +661,9 @@ const logStudySession = async (req, res) => {
461661

462662
module.exports = {
463663
getUserStatistics,
664+
getQuizPerformance,
665+
getNoteProgress,
666+
getStudySessions,
464667
updateTopicProgress,
465668
updateQuizProgress,
466669
updateNoteProgress,

0 commit comments

Comments
 (0)