Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: quiz evaluation improvements #357

Merged
merged 3 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/api/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,12 @@ module.exports = {
],
"unused-imports/no-unused-imports": "error",
},
overrides: [
{
files: ["src/seed/**/*.ts", "src/stripe/stripe.controller.ts", "test/jest-setup.ts"],
rules: {
"no-console": "off",
},
},
],
};
16 changes: 0 additions & 16 deletions apps/api/src/chapter/chapter.type.ts

This file was deleted.

4 changes: 2 additions & 2 deletions apps/api/src/chapter/repositories/adminChapter.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { chapters, courses, lessons, questionAnswerOptions, questions } from "sr

import type { UpdateChapterBody } from "../schemas/chapter.schema";
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
import type { AdminLessonWithContentSchema, QuestionSchema } from "src/lesson/lesson.schema";
import type { AdminLessonWithContentSchema, AdminQuestionBody } from "src/lesson/lesson.schema";
import type { LessonTypes } from "src/lesson/lesson.type";
import type * as schema from "src/storage/schema";

Expand Down Expand Up @@ -82,7 +82,7 @@ export class AdminChapterRepository {
fileType: sql<string>`${lessons.fileType}`,
displayOrder: sql<number>`${lessons.displayOrder}`,
isExternal: sql<boolean>`${lessons.isExternal}`,
questions: sql<QuestionSchema[]>`
questions: sql<AdminQuestionBody[]>`
(
SELECT ARRAY(
SELECT json_build_object(
Expand Down
5 changes: 0 additions & 5 deletions apps/api/src/common/states.ts

This file was deleted.

29 changes: 4 additions & 25 deletions apps/api/src/courses/course.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,13 +423,7 @@ export class CourseService {
)
.orderBy(chapters.displayOrder);

// TODO: temporary fix
const getImageUrl = async (url: string) => {
if (!url || url.startsWith("https://")) return url ?? "";
return await this.fileService.getFileUrl(url);
};

const thumbnailUrl = await getImageUrl(course.thumbnailS3Key);
const thumbnailUrl = await this.fileService.getFileUrl(course.thumbnailS3Key);

return {
...course,
Expand Down Expand Up @@ -480,12 +474,7 @@ export class CourseService {
.where(and(eq(chapters.courseId, id), isNotNull(chapters.title)))
.orderBy(chapters.displayOrder);

const getImageUrl = async (url: string) => {
if (!url || url.startsWith("https://")) return url;
return await this.fileService.getFileUrl(url);
};

const thumbnailS3SingedUrl = await getImageUrl(course.thumbnailS3Key);
const thumbnailS3SingedUrl = await this.fileService.getFileUrl(course.thumbnailS3Key);

const updatedCourseLessonList = await Promise.all(
courseChapterList?.map(async (chapter) => {
Expand Down Expand Up @@ -547,12 +536,7 @@ export class CourseService {
)
.orderBy(chapters.displayOrder);

const getImageUrl = async (url: string) => {
if (!url || url.startsWith("https://")) return url;
return await this.fileService.getFileUrl(url);
};

const thumbnailS3SingedUrl = await getImageUrl(course.thumbnailS3Key);
const thumbnailS3SingedUrl = await this.fileService.getFileUrl(course.thumbnailS3Key);

return {
...course,
Expand Down Expand Up @@ -591,11 +575,6 @@ export class CourseService {
conditions.push(inArray(courses.id, availableCourseIds));
}

const getImageUrl = async (url: string) => {
if (!url || url.startsWith("https://")) return url;
return await this.fileService.getFileUrl(url);
};

const teacherCourses = await this.db
.select({
id: courses.id,
Expand Down Expand Up @@ -649,7 +628,7 @@ export class CourseService {
teacherCourses.map(async (course) => ({
...course,
thumbnailUrl: course.thumbnailUrl
? await getImageUrl(course.thumbnailUrl)
? await this.fileService.getFileUrl(course.thumbnailUrl)
: course.thumbnailUrl,
})),
);
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/file/file.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const REDIS_TTL = 59 * 60 * 1000;
export const MAX_FILE_SIZE = 20 * 1024 * 1024;
14 changes: 8 additions & 6 deletions apps/api/src/file/file.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { BadRequestException, ConflictException, Inject, Injectable } from "@nes

import { S3Service } from "src/s3/s3.service";

import { MAX_FILE_SIZE, REDIS_TTL } from "./file.constants";

import type { createCache } from "cache-manager";

@Injectable()
Expand All @@ -12,14 +14,15 @@ export class FileService {
) {}

async getFileUrl(fileKey: string): Promise<string> {
if (fileKey.startsWith("https://")) return fileKey;

try {
const cachedUrl = await this.cacheManager.get<string>(fileKey);
if (cachedUrl) return cachedUrl;

const signedUrl = await this.s3Service.getSignedUrl(fileKey);
// TODO: extract to config constant
const ttl = 59 * 60 * 1000;
await this.cacheManager.set(fileKey, signedUrl, ttl);

await this.cacheManager.set(fileKey, signedUrl, REDIS_TTL);

return signedUrl;
} catch (error) {
Expand All @@ -33,7 +36,6 @@ export class FileService {
throw new BadRequestException("No file uploaded");
}

const maxSize = 50 * 1024 * 1024; // 50MB
const allowedMimeTypes = [
"image/jpeg",
"image/png",
Expand All @@ -43,9 +45,9 @@ export class FileService {
"video/quicktime",
];

if (file.size > maxSize) {
if (file.size > MAX_FILE_SIZE) {
throw new BadRequestException(
`File size exceeds the maximum allowed size of ${maxSize} bytes`,
`File size exceeds the maximum allowed size of ${MAX_FILE_SIZE} bytes`,
);
}

Expand Down
10 changes: 2 additions & 8 deletions apps/api/src/lesson/lesson.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,11 @@ export const optionSchema = Type.Object({
});

export const questionSchema = Type.Object({
...adminQuestionSchema.properties,
id: UUIDSchema,
type: Type.Enum(QUESTION_TYPE),
description: Type.Optional(Type.Union([Type.String(), Type.Null()])),
title: Type.String(),
displayOrder: Type.Optional(Type.Number()),
photoS3Key: Type.Optional(Type.Union([Type.String(), Type.Null()])),
solutionExplanation: Type.Union([Type.String(), Type.Null()]),
options: Type.Optional(Type.Array(optionSchema)),
passQuestion: Type.Union([Type.Boolean(), Type.Null()]),
solutionExplanation: Type.Union([Type.String(), Type.Null()]),
});

export const lessonSchema = Type.Object({
Expand Down Expand Up @@ -161,12 +157,10 @@ export type CreateLessonBody = Static<typeof createLessonSchema>;
export type UpdateLessonBody = Static<typeof updateLessonSchema>;
export type UpdateQuizLessonBody = Static<typeof updateQuizLessonSchema>;
export type CreateQuizLessonBody = Static<typeof createQuizLessonSchema>;
// TODO: duplicate
export type OptionBody = Static<typeof optionSchema>;
export type AdminOptionBody = Static<typeof adminOptionSchema>;
export type AdminQuestionBody = Static<typeof adminQuestionSchema>;
export type QuestionBody = Static<typeof questionSchema>;
export type QuestionSchema = Static<typeof adminQuestionSchema>;
export type LessonShow = Static<typeof lessonShowSchema>;
export type LessonSchema = Static<typeof lessonSchema>;
export type AnswerQuestionBody = Static<typeof answerQuestionsForLessonBody>;
Expand Down
50 changes: 35 additions & 15 deletions apps/api/src/lesson/repositories/lesson.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
courses,
lessons,
questions,
quizAttempts,
studentCourses,
studentLessonProgress,
} from "src/storage/schema";
Expand Down Expand Up @@ -41,21 +42,21 @@ export class LessonRepository {
isFreemium: sql<boolean>`${chapters.isFreemium}`,
isEnrolled: sql<boolean>`CASE WHEN ${studentCourses.id} IS NULL THEN FALSE ELSE TRUE END`,
nextLessonId: sql<string | null>`
COALESCE(
(
SELECT l2.id
FROM ${lessons} l2
JOIN ${chapters} c ON c.id = l2.chapter_id
LEFT JOIN ${studentLessonProgress} slp ON slp.lesson_id = l2.id AND slp.student_id = ${userId}
WHERE c.course_id = ${chapters.courseId}
AND l2.id != ${lessons.id}
AND slp.completed_at IS NULL
ORDER BY c.display_order, l2.display_order
LIMIT 1
),
NULL
)
`,
COALESCE(
(
SELECT l2.id
FROM ${lessons} l2
JOIN ${chapters} c ON c.id = l2.chapter_id
LEFT JOIN ${studentLessonProgress} slp ON slp.lesson_id = l2.id AND slp.student_id = ${userId}
WHERE c.course_id = ${chapters.courseId}
AND l2.id != ${lessons.id}
AND slp.completed_at IS NULL
ORDER BY c.display_order, l2.display_order
LIMIT 1
),
NULL
)
`,
})
.from(lessons)
.leftJoin(chapters, eq(chapters.id, lessons.chapterId))
Expand Down Expand Up @@ -244,6 +245,25 @@ export class LessonRepository {
.where(and(eq(chapters.isPublished, true), eq(lessons.id, id)));
}

async getQuizResult(lessonId: UUIDType, quizScore: number, userId: UUIDType) {
return await this.db
.select({
score: sql<number>`${quizAttempts.score}`,
correctAnswerCount: sql<number>`${quizAttempts.correctAnswers}`,
wrongAnswerCount: sql<number>`${quizAttempts.wrongAnswers}`,
})
.from(quizAttempts)
.where(
and(
eq(quizAttempts.lessonId, lessonId),
eq(quizAttempts.userId, userId),
eq(quizAttempts.score, quizScore),
),
)
.orderBy(desc(quizAttempts.createdAt))
.limit(1);
}

// async retireQuizProgress(
// courseId: UUIDType,
// lessonId: UUIDType,
Expand Down
Loading
Loading