Skip to content

Commit

Permalink
feat: New Courses View (#288)
Browse files Browse the repository at this point in the history
* feat: New Course View

* refactor: apply review feedback
  • Loading branch information
piotr-pajak authored Dec 19, 2024
1 parent 252c8ea commit dc9d391
Show file tree
Hide file tree
Showing 77 changed files with 1,940 additions and 974 deletions.
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}`,
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

0 comments on commit dc9d391

Please sign in to comment.