Skip to content

Commit

Permalink
feat: quiz evaluation improvements (#357)
Browse files Browse the repository at this point in the history
* feat: change way of store student answers

* feat: changing hierarchy in stripe directory

* feat: refactor lesson service
  • Loading branch information
wielopolski authored Jan 10, 2025
1 parent 80fed6a commit eec6462
Show file tree
Hide file tree
Showing 18 changed files with 201 additions and 227 deletions.
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

0 comments on commit eec6462

Please sign in to comment.