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: New Courses View #288

Merged
merged 2 commits into from
Dec 19, 2024
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
29 changes: 7 additions & 22 deletions apps/api/src/chapter/adminChapter.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Inject, Injectable, NotFoundException } from "@nestjs/common";
import { eq, gte, lte, sql } from "drizzle-orm";
import { eq, sql } from "drizzle-orm";

import { DatabasePg } from "src/common";
import { FileService } from "src/file/file.service";
Expand Down Expand Up @@ -230,27 +230,12 @@ export class AdminChapterService {

const newDisplayOrder = chapterObject.displayOrder;

await this.db.transaction(async (trx) => {
await trx
.update(chapters)
.set({
displayOrder: sql`CASE
WHEN ${eq(chapters.id, chapterToUpdate.id)}
THEN ${newDisplayOrder}
WHEN ${newDisplayOrder < oldDisplayOrder}
AND ${gte(chapters.displayOrder, newDisplayOrder)}
AND ${lte(chapters.displayOrder, oldDisplayOrder)}
THEN ${chapters.displayOrder} + 1
WHEN ${newDisplayOrder > oldDisplayOrder}
AND ${lte(chapters.displayOrder, newDisplayOrder)}
AND ${gte(chapters.displayOrder, oldDisplayOrder)}
THEN ${chapters.displayOrder} - 1
ELSE ${chapters.displayOrder}
END
`,
})
.where(eq(chapters.courseId, chapterToUpdate.courseId));
});
await this.adminChapterRepository.changeChapterDisplayOrder(
chapterToUpdate.courseId,
chapterToUpdate.id,
oldDisplayOrder,
newDisplayOrder,
);
}
// async updateLesson(id: string, body: UpdateLessonBody) {
// const [lesson] = await this.adminChapterRepository.updateLesson(id, body);
Expand Down
48 changes: 25 additions & 23 deletions apps/api/src/chapter/chapter.controller.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { Body, Controller, Delete, Patch, Post, Query, UseGuards } from "@nestjs/common";
import { Body, Controller, Delete, Get, Patch, Post, Query, UseGuards } from "@nestjs/common";
import { Type } from "@sinclair/typebox";
import { Validate } from "nestjs-typebox";

import { baseResponse, BaseResponse, UUIDSchema, type UUIDType } from "src/common";
import { Roles } from "src/common/decorators/roles.decorator";
import { CurrentUser } from "src/common/decorators/user.decorator";
import { RolesGuard } from "src/common/guards/roles.guard";
import { USER_ROLES } from "src/user/schemas/userRoles";
import { USER_ROLES, type UserRole } from "src/user/schemas/userRoles";

import { AdminChapterService } from "./adminChapter.service";
import { ChapterService } from "./chapter.service";
import { CreateChapterBody, createChapterSchema } from "./schemas/chapter.schema";
import {
CreateChapterBody,
createChapterSchema,
showChapterSchema,
} from "./schemas/chapter.schema";

import type { ShowChapterResponse } from "./schemas/chapter.schema";

@Controller("chapter")
@UseGuards(RolesGuard)
Expand All @@ -20,25 +26,21 @@ export class ChapterController {
private readonly adminChapterService: AdminChapterService,
) {}

// @Get("lesson")
// @Roles(...Object.values(USER_ROLES))
// @Validate({
// request: [
// { type: "query", name: "id", schema: UUIDSchema, required: true },
// { type: "query", name: "courseId", schema: UUIDSchema, required: true },
// ],
// response: baseResponse(showLessonSchema),
// })
// async getLesson(
// @Query("id") id: UUIDType,
// @Query("courseId") courseId: UUIDType,
// @CurrentUser("role") userRole: UserRole,
// @CurrentUser("userId") userId: UUIDType,
// ): Promise<BaseResponse<ShowLessonResponse>> {
// return new BaseResponse(
// await this.lessonsService.getLesson(id, courseId, userId, userRole === USER_ROLES.ADMIN),
// );
// }
@Get()
@Roles(...Object.values(USER_ROLES))
@Validate({
request: [{ type: "query", name: "id", schema: UUIDSchema, required: true }],
response: baseResponse(showChapterSchema),
})
async getChapterWithLesson(
@Query("id") id: UUIDType,
@CurrentUser("role") userRole: UserRole,
@CurrentUser("userId") userId: UUIDType,
): Promise<BaseResponse<ShowChapterResponse>> {
return new BaseResponse(
await this.chapterService.getChapterWithLessons(id, userId, userRole === USER_ROLES.ADMIN),
);
}

// @Get("lesson/:id")
// @Roles(USER_ROLES.TEACHER, USER_ROLES.ADMIN)
Expand Down Expand Up @@ -141,7 +143,7 @@ export class ChapterController {
// }

// @Delete(":courseId/:lessonId")
// @Roles(USER_ROLES.TEACHER, USER_ROLES.ADMIN)
// @Roles(USER_ROLES.teacher, USER_ROLES.admin)
// @Validate({
// request: [
// { type: "param", name: "courseId", schema: UUIDSchema },
Expand Down
86 changes: 19 additions & 67 deletions apps/api/src/chapter/chapter.service.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,38 @@
import { Inject, Injectable } from "@nestjs/common";
import { Inject, Injectable, NotFoundException, UnauthorizedException } from "@nestjs/common";
import { EventBus } from "@nestjs/cqrs";

import { DatabasePg } from "src/common";
import { LessonRepository } from "src/lesson/lesson.repository";

import { ChapterRepository } from "./repositories/chapter.repository";

import type { ShowChapterResponse } from "src/chapter/schemas/chapter.schema";
import type { UUIDType } from "src/common";

@Injectable()
export class ChapterService {
constructor(
@Inject("DB") private readonly db: DatabasePg,
private readonly chapterRepository: ChapterRepository,
private readonly lessonRepository: LessonRepository,
private readonly eventBus: EventBus,
) {}

// async getChapterWithDetails(id: UUIDType, userId: UUIDType, isStudent: boolean) {
// const hasCourseAccess = await this.chapterRepository.checkChapterAssignment(id, userId);
// if (!hasCourseAccess && isStudent) throw new Error("You don't have access to this chapter");

// const [chapter] = await this.chapterRepository.getChapterWithDetails(id, userId);
// if (!chapter) throw new Error("Chapter not found");

// return chapter;
// }

// async getChapter(
// id: UUIDType,
// userId: UUIDType,
// isAdmin?: boolean,
// ): Promise<ShowChapterResponse> {
// const [courseAccess] = await this.chapterRepository.checkChapterAssignment(id, userId);
// const chapter = await this.chapterRepository.getChapterForUser(id, userId);

// if (!isAdmin && !courseAccess && !chapter.isFreemium)
// throw new UnauthorizedException("You don't have access to this lesson");

// if (!chapter) throw new NotFoundException("Lesson not found");

// // const chapterProgress = await this.chapterRepository.getChapterProgressForStudent(
// // chapter.id,
// // userId,
// // );

// // if (lesson.type !== LESSON_TYPE.quiz.key) {
// // const lessonItems = await this.getLessonItems(chapter.id, courseId, userId);

// // const completableLessonItems = lessonItems.filter(
// // (item) => item.lessonItemType !== LESSON_ITEM_TYPE.text_block.key,
// // );

// // return {
// // ...lesson,
// // imageUrl,
// // lessonItems: lessonItems,
// // lessonProgress:
// // completableLessonItems.length === 0
// // ? ChapterProgress.notStarted
// // : completableLessonItems.length > 0
// // ? ChapterProgress.inProgress
// // : ChapterProgress.completed,
// // itemsCompletedCount: completedLessonItems.length,
// // };
// // }

// // const lessonProgress = await this.chapterRepository.lessonProgress(
// // courseId,
// // lesson.id,
// // userId,
// // true,
// // );
async getChapterWithLessons(
id: UUIDType,
userId: UUIDType,
isAdmin?: boolean,
): Promise<ShowChapterResponse> {
const [courseAccess] = await this.chapterRepository.checkChapterAssignment(id, userId);
const chapter = await this.chapterRepository.getChapterForUser(id, userId);

// // if (!lessonProgress && !isAdmin && !lesson.isFree)
// // throw new NotFoundException("Lesson progress not found");
if (!isAdmin && !courseAccess && !chapter.isFreemium)
throw new UnauthorizedException("You don't have access to this lesson");

// // const isAdminOrFreeLessonWithoutLessonProgress = (isAdmin || lesson.isFree) && !lessonProgress;
if (!chapter) throw new NotFoundException("Chapter not found");

// // const questionLessonItems = await this.getLessonQuestions(
// // lesson,
// // courseId,
// // userId,
// // isAdminOrFreeLessonWithoutLessonProgress ? false : lessonProgress.quizCompleted,
// // );
const chapterLessonList = await this.lessonRepository.getLessonsByChapterId(id);

// return chapter;
// }
return { ...chapter, lessons: chapterLessonList };
}
}
31 changes: 30 additions & 1 deletion apps/api/src/chapter/repositories/adminChapter.repository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Inject, Injectable } from "@nestjs/common";
import { eq, and, sql } from "drizzle-orm";
import { and, eq, gte, lte, sql } from "drizzle-orm";

import { DatabasePg, type UUIDType } from "src/common";
import { chapters, lessons, questionAnswerOptions, questions } from "src/storage/schema";
Expand All @@ -16,6 +16,35 @@ export class AdminChapterRepository {
return await this.db.select().from(chapters).where(eq(chapters.id, chapterId));
}

async changeChapterDisplayOrder(
courseId: UUIDType,
chapterId: UUIDType,
oldDisplayOrder: number,
newDisplayOrder: number,
) {
await this.db.transaction(async (trx) => {
await trx
.update(chapters)
.set({
displayOrder: sql`CASE
WHEN ${eq(chapters.id, chapterId)}
THEN ${newDisplayOrder}
WHEN ${newDisplayOrder < oldDisplayOrder}
AND ${gte(chapters.displayOrder, newDisplayOrder)}
AND ${lte(chapters.displayOrder, oldDisplayOrder)}
THEN ${chapters.displayOrder} + 1
WHEN ${newDisplayOrder > oldDisplayOrder}
AND ${lte(chapters.displayOrder, newDisplayOrder)}
AND ${gte(chapters.displayOrder, oldDisplayOrder)}
THEN ${chapters.displayOrder} - 1
ELSE ${chapters.displayOrder}
END
`,
})
.where(eq(chapters.courseId, courseId));
});
}

// async getLessons(conditions: any[], sortOrder: any) {
// return await this.db
// .select({
Expand Down
31 changes: 13 additions & 18 deletions apps/api/src/chapter/repositories/chapter.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { and, eq, isNotNull, sql } from "drizzle-orm";

import { DatabasePg, type UUIDType } from "src/common";
import { chapters, lessons, studentChapterProgress, studentCourses } from "src/storage/schema";
import { PROGRESS_STATUSES } from "src/utils/types/progress.type";

import type { ProgressStatus } from "src/utils/types/progress.type";

@Injectable()
export class ChapterRepository {
constructor(@Inject("DB") private readonly db: DatabasePg) {}

async getChapterWithDetails(id: UUIDType, userId: UUIDType, isStudent: boolean) {
return await this.db
return this.db
.select({
id: chapters.id,
title: chapters.title,
Expand Down Expand Up @@ -58,16 +61,22 @@ export class ChapterRepository {
.where(and(eq(chapters.isPublished, true), eq(chapters.id, id)));
}

// TODO: check this functions \/
async getChapterForUser(id: UUIDType, userId: UUIDType) {
const [lesson] = await this.db
const [chapter] = await this.db
.select({
displayOrder: sql<number>`${lessons.displayOrder}`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move it below, please 🙏🏻

id: chapters.id,
title: chapters.title,
isFreemium: chapters.isFreemium,
enrolled: sql<boolean>`CASE WHEN ${studentCourses.id} IS NOT NULL THEN true ELSE false END`,
lessonCount: chapters.lessonCount,
completedLessonCount: sql<number>`COALESCE(${studentChapterProgress.completedLessonCount}, 0)`,
progress: sql<ProgressStatus>`
CASE ${studentChapterProgress.completedAt} IS NOT NULL
THEN ${PROGRESS_STATUSES.COMPLETED}
WHEN ${studentChapterProgress.completedAt} IS NULL AND ${studentChapterProgress.completedLessonCount} < 0
THEN ${PROGRESS_STATUSES.IN_PROGRESS}
ELSE ${PROGRESS_STATUSES.NOT_STARTED}`,
})
.from(chapters)
.leftJoin(
Expand All @@ -84,7 +93,7 @@ export class ChapterRepository {
)
.where(and(eq(chapters.id, id), eq(chapters.isPublished, true)));

return lesson;
return chapter;
}

async getChapter(id: UUIDType) {
Expand All @@ -100,18 +109,4 @@ export class ChapterRepository {

return chapter;
}

async getChapterProgressForStudent(chapterId: UUIDType, userId: UUIDType) {
const [chapterProgress] = await this.db
.select({})
.from(studentChapterProgress)
.where(
and(
eq(studentChapterProgress.studentId, userId),
eq(studentChapterProgress.chapterId, chapterId),
),
);

return chapterProgress;
}
}
Loading
Loading