Skip to content

Commit

Permalink
refactor: statistics module improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
piotr-pajak committed Nov 21, 2024
1 parent a8e6305 commit 7a5d926
Show file tree
Hide file tree
Showing 9 changed files with 547 additions and 350 deletions.
1 change: 1 addition & 0 deletions apps/api/src/courses/courses.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ export class CoursesService {

if (!course) throw new NotFoundException("Course not found");

// TODO: to remove and start use getLessonsDetails form lessonsRepository
const courseLessonList = await this.db
.select({
id: lessons.id,
Expand Down
152 changes: 120 additions & 32 deletions apps/api/src/lessons/repositories/lessons.repository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Inject, Injectable } from "@nestjs/common";
import { and, count, eq, inArray, sql } from "drizzle-orm";
import { and, count, desc, eq, inArray, isNotNull, sql } from "drizzle-orm";

import { DatabasePg, type UUIDType } from "src/common";
import { STATES } from "src/common/states";
Expand All @@ -19,6 +19,7 @@ import {
} from "src/storage/schema";

import { LESSON_TYPE } from "../lesson.type";
import { LessonProgress } from "../schemas/lesson.types";

import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
import type * as schema from "src/storage/schema";
Expand All @@ -36,11 +37,11 @@ export class LessonsRepository {
imageUrl: sql<string>`${lessons.imageUrl}`,
type: sql<string>`${lessons.type}`,
isSubmitted: sql<boolean>`
CASE
CASE
WHEN ${studentLessonsProgress.quizCompleted} IS NOT NULL THEN ${studentLessonsProgress.quizCompleted}
ELSE FALSE
END
`,
ELSE FALSE
END
`,
isFree: courseLessons.isFree,
enrolled: sql<boolean>`CASE WHEN ${studentCourses.id} IS NOT NULL THEN true ELSE false END`,
})
Expand Down Expand Up @@ -85,12 +86,12 @@ export class LessonsRepository {
questionData: questions,
displayOrder: lessonItems.displayOrder,
passQuestion: sql<boolean | null>`
CASE
CASE
WHEN ${lessonType} = ${LESSON_TYPE.quiz.key} AND ${lessonRated} THEN
${studentQuestionAnswers.isCorrect}
ELSE null
END
`,
${studentQuestionAnswers.isCorrect}
ELSE null
END
`,
})
.from(lessonItems)
.leftJoin(
Expand Down Expand Up @@ -169,27 +170,27 @@ export class LessonsRepository {
optionText: questionAnswerOptions.optionText,
position: questionAnswerOptions.position,
isStudentAnswer: sql<boolean | null>`
CASE
CASE
WHEN ${studentQuestionAnswers.id} IS NULL THEN NULL
WHEN ${studentQuestionAnswers.answer}->>CAST(${questionAnswerOptions.position} AS text) = ${questionAnswerOptions.optionText} AND
${questions.questionType} IN (${QUESTION_TYPE.fill_in_the_blanks_dnd.key}, ${QUESTION_TYPE.fill_in_the_blanks_text.key})
THEN TRUE
WHEN EXISTS (
SELECT 1
FROM jsonb_object_keys(${studentQuestionAnswers.answer}) AS key
WHERE ${studentQuestionAnswers.answer}->key = to_jsonb(${questionAnswerOptions.optionText})
) AND ${questions.questionType} NOT IN (${QUESTION_TYPE.fill_in_the_blanks_dnd.key}, ${QUESTION_TYPE.fill_in_the_blanks_text.key})
THEN TRUE
ELSE FALSE
END
`,
WHEN ${studentQuestionAnswers.answer}->>CAST(${questionAnswerOptions.position} AS text) = ${questionAnswerOptions.optionText} AND
${questions.questionType} IN (${QUESTION_TYPE.fill_in_the_blanks_dnd.key}, ${QUESTION_TYPE.fill_in_the_blanks_text.key})
THEN TRUE
WHEN EXISTS (
SELECT 1
FROM jsonb_object_keys(${studentQuestionAnswers.answer}) AS key
WHERE ${studentQuestionAnswers.answer}->key = to_jsonb(${questionAnswerOptions.optionText})
) AND ${questions.questionType} NOT IN (${QUESTION_TYPE.fill_in_the_blanks_dnd.key}, ${QUESTION_TYPE.fill_in_the_blanks_text.key})
THEN TRUE
ELSE FALSE
END
`,
isCorrect: sql<boolean | null>`
CASE
CASE
WHEN ${lessonType} = 'quiz' AND ${lessonRated} THEN
${questionAnswerOptions.isCorrect}
ELSE NULL
END
`,
${questionAnswerOptions.isCorrect}
ELSE NULL
END
`,
})
.from(questionAnswerOptions)
.leftJoin(questions, eq(questionAnswerOptions.questionId, questions.id))
Expand Down Expand Up @@ -256,11 +257,11 @@ export class LessonsRepository {
const [lessonProgress] = await this.db
.select({
quizCompleted: sql<boolean>`
CASE
CASE
WHEN ${studentLessonsProgress.quizCompleted} THEN
${studentLessonsProgress.quizCompleted}
ELSE false
END`,
${studentLessonsProgress.quizCompleted}
ELSE false
END`,
lessonItemCount: studentLessonsProgress.lessonItemCount,
completedLessonItemCount: studentLessonsProgress.completedLessonItemCount,
quizScore: studentLessonsProgress.quizScore,
Expand Down Expand Up @@ -328,6 +329,78 @@ export class LessonsRepository {
return quizScore.quizScore;
}

async getLessonsDetails(userId: UUIDType, courseId: UUIDType, lessonId?: UUIDType) {
const conditions = [
eq(courseLessons.courseId, courseId),
eq(lessons.archived, false),
eq(lessons.state, STATES.published),
isNotNull(lessons.id),
isNotNull(lessons.title),
isNotNull(lessons.description),
isNotNull(lessons.imageUrl),
];
if (lessonId) conditions.push(eq(lessons.id, lessonId));

return await this.db
.select({
id: lessons.id,
title: lessons.title,
type: lessons.type,
isSubmitted: sql<boolean>`
EXISTS (
SELECT 1
FROM ${studentLessonsProgress}
WHERE ${studentLessonsProgress.lessonId} = ${lessons.id}
AND ${studentLessonsProgress.courseId} = ${courseId}
AND ${studentLessonsProgress.studentId} = ${userId}
AND ${studentLessonsProgress.quizCompleted}
)::BOOLEAN`,
description: sql<string>`${lessons.description}`,
imageUrl: sql<string>`${lessons.imageUrl}`,
itemsCount: sql<number>`
(SELECT COUNT(*)
FROM ${lessonItems}
WHERE ${lessonItems.lessonId} = ${lessons.id}
AND ${lessonItems.lessonItemType} != 'text_block')::INTEGER`,
itemsCompletedCount: sql<number>`
(SELECT COUNT(*)
FROM ${studentCompletedLessonItems}
WHERE ${studentCompletedLessonItems.lessonId} = ${lessons.id}
AND ${studentCompletedLessonItems.courseId} = ${courseId}
AND ${studentCompletedLessonItems.studentId} = ${userId})::INTEGER`,
lessonProgress: sql<"completed" | "in_progress" | "not_started">`
(CASE
WHEN (
SELECT COUNT(*)
FROM ${lessonItems}
WHERE ${lessonItems.lessonId} = ${lessons.id}
AND ${lessonItems.lessonItemType} != 'text_block'
) = (
SELECT COUNT(*)
FROM ${studentCompletedLessonItems}
WHERE ${studentCompletedLessonItems.lessonId} = ${lessons.id}
AND ${studentCompletedLessonItems.courseId} = ${courseId}
AND ${studentCompletedLessonItems.studentId} = ${userId}
)
THEN ${LessonProgress.completed}
WHEN (
SELECT COUNT(*)
FROM ${studentCompletedLessonItems}
WHERE ${studentCompletedLessonItems.lessonId} = ${lessons.id}
AND ${studentCompletedLessonItems.courseId} = ${courseId}
AND ${studentCompletedLessonItems.studentId} = ${userId}
) > 0
THEN ${LessonProgress.inProgress}
ELSE ${LessonProgress.notStarted}
END)
`,
isFree: courseLessons.isFree,
})
.from(courseLessons)
.innerJoin(lessons, eq(courseLessons.lessonId, lessons.id))
.where(and(...conditions));
}

async completeQuiz(
courseId: UUIDType,
lessonId: UUIDType,
Expand Down Expand Up @@ -452,6 +525,21 @@ export class LessonsRepository {
.limit(1);
}

async getLastLessonItemForUser(userId: UUIDType) {
const [lastLessonItem] = await this.db
.select({
id: studentCompletedLessonItems.lessonItemId,
lessonId: studentCompletedLessonItems.lessonId,
courseId: studentCompletedLessonItems.courseId,
})
.from(studentCompletedLessonItems)
.where(eq(studentCompletedLessonItems.studentId, userId))
.orderBy(desc(studentCompletedLessonItems.updatedAt))
.limit(1);

return lastLessonItem;
}

async removeQuestionsAnswer(
courseId: UUIDType,
lessonId: UUIDType,
Expand Down
13 changes: 11 additions & 2 deletions apps/api/src/statistics/api/statistics.controller.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { Controller, Get } from "@nestjs/common";
import { Validate } from "nestjs-typebox";

import { BaseResponse, UUIDType } from "src/common";
import { baseResponse, BaseResponse, UUIDType } from "src/common";
import { CurrentUser } from "src/common/decorators/user.decorator";

import { UserStatsSchema } from "../schemas/userStats.schema";
import { StatisticsService } from "../statistics.service";

import type { UserStats } from "../schemas/userStats.schema";

@Controller("statistics")
export class StatisticsController {
constructor(private statisticsService: StatisticsService) {}

@Get()
async getUserStatistics(@CurrentUser("userId") currentUserId: UUIDType) {
@Validate({
response: baseResponse(UserStatsSchema),
})
async getUserStatistics(
@CurrentUser("userId") currentUserId: UUIDType,
): Promise<BaseResponse<UserStats>> {
return new BaseResponse(await this.statisticsService.getUserStats(currentUserId));
}
}
12 changes: 8 additions & 4 deletions apps/api/src/statistics/handlers/statistics.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Injectable } from "@nestjs/common";
import { EventsHandler } from "@nestjs/cqrs";
import { match } from "ts-pattern";

import { QuizCompletedEvent, UserActivityEvent, CourseStartedEvent } from "src/events";
import { CourseStartedEvent, QuizCompletedEvent, UserActivityEvent } from "src/events";
import { StatisticsRepository } from "src/statistics/repositories/statistics.repository";

import { StatisticsRepository } from "../repositories/statistics.repository";
import { StatisticsService } from "../statistics.service";

import type { IEventHandler } from "@nestjs/cqrs";

Expand All @@ -13,7 +14,10 @@ type StatisticsEvent = QuizCompletedEvent | UserActivityEvent | CourseStartedEve
@Injectable()
@EventsHandler(QuizCompletedEvent, UserActivityEvent, CourseStartedEvent)
export class StatisticsHandler implements IEventHandler<QuizCompletedEvent | UserActivityEvent> {
constructor(private readonly statisticsRepository: StatisticsRepository) {}
constructor(
private readonly statisticsRepository: StatisticsRepository,
private readonly statisticsService: StatisticsService,
) {}

async handle(event: StatisticsEvent) {
try {
Expand Down Expand Up @@ -50,6 +54,6 @@ export class StatisticsHandler implements IEventHandler<QuizCompletedEvent | Use
}

private async handleUserActivity(event: UserActivityEvent) {
await this.statisticsRepository.updateUserActivity(event.userId);
await this.statisticsService.updateUserActivity(event.userId);
}
}
Loading

0 comments on commit 7a5d926

Please sign in to comment.