From 8d8f471585089ce941d2b8225ceb5fc596ef9023 Mon Sep 17 00:00:00 2001 From: Matin Date: Tue, 11 Feb 2025 14:17:04 +0330 Subject: [PATCH] chore: complete learning module --- src/app.module.ts | 2 + src/common/enums/message.enum.ts | 15 +++ .../DTO/create-learning-category.dto.ts | 11 ++ .../learning/DTO/create-learning.dto.ts | 40 +++++++ .../entities/learning-category.entity.ts | 13 +++ .../entities/learning-progress.entity.ts | 17 +++ .../learning/entities/learning.entity.ts | 29 +++++ src/modules/learning/learning.controller.ts | 73 ++++++++++++ src/modules/learning/learning.module.ts | 20 ++++ .../learning/providers/learning.service.ts | 106 ++++++++++++++++++ .../learning-category.repository.ts | 12 ++ .../learning-progress.repository.ts | 12 ++ .../repositories/learning.repository.ts | 12 ++ src/modules/users/entities/user.entity.ts | 4 + 14 files changed, 366 insertions(+) create mode 100644 src/modules/learning/DTO/create-learning-category.dto.ts create mode 100644 src/modules/learning/DTO/create-learning.dto.ts create mode 100644 src/modules/learning/entities/learning-category.entity.ts create mode 100644 src/modules/learning/entities/learning-progress.entity.ts create mode 100644 src/modules/learning/entities/learning.entity.ts create mode 100644 src/modules/learning/learning.controller.ts create mode 100644 src/modules/learning/learning.module.ts create mode 100644 src/modules/learning/providers/learning.service.ts create mode 100644 src/modules/learning/repositories/learning-category.repository.ts create mode 100644 src/modules/learning/repositories/learning-progress.repository.ts create mode 100644 src/modules/learning/repositories/learning.repository.ts diff --git a/src/app.module.ts b/src/app.module.ts index 8ab914f..bef2c20 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -18,6 +18,7 @@ import { ContactUsModule } from "./modules/contact-us/contact-us.module"; import { CriticismModule } from "./modules/criticisms/criticisms.module"; import { DanakServicesModule } from "./modules/danak-services/danak-services.module"; import { InvoicesModule } from "./modules/invoices/invoices.module"; +import { LearningModule } from "./modules/learning/learning.module"; import { NotificationModule } from "./modules/notifications/notifications.module"; import { PaymentsModule } from "./modules/payments/payments.module"; import { SettingModule } from "./modules/settings/settings.module"; @@ -50,6 +51,7 @@ import { WalletsModule } from "./modules/wallets/wallets.module"; NotificationModule, InvoicesModule, SubscriptionsModule, + LearningModule, ], controllers: [], providers: [], diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index bce67d1..b2c3b06 100644 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -291,3 +291,18 @@ export const enum SubscriptionMessage { NOT_FOUND = "اشتراک یافت نشد", UPDATED = "اشتراک با موفقیت به روز رسانی شد", } + +export const enum LearningMessage { + TITLE_REQUIRED = "عنوان الزامی است", + TITLE_STRING = "عنوان باید یک رشته باشد", + DESCRIPTION_REQUIRED = "توضیحات الزامی است", + DESCRIPTION_STRING = "توضیحات باید یک رشته باشد", + COVER_URL_REQUIRED = "آدرس تصویر کاور الزامی است", + COVER_URL_INVALID = "آدرس تصویر کاور معتبر نیست", + VIDEO_URL_INVALID = "آدرس ویدیو معتبر نیست", + VIDEO_DURATION_INVALID = "مدت زمان ویدیو معتبر نیست", + NOT_FOUND = "آموزشی با این مشخصات یافت نشد", + CATEGORY_NOT_FOUND = "دسته بندی با این مشخصات یافت نشد", + CATEGORY_MUST_BE_ID = "دسته‌بندی باید شناسه باشد", + CATEGORY_MUST_BE_UUID = "دسته‌بندی باید UUID باشد", +} diff --git a/src/modules/learning/DTO/create-learning-category.dto.ts b/src/modules/learning/DTO/create-learning-category.dto.ts new file mode 100644 index 0000000..3dac8b7 --- /dev/null +++ b/src/modules/learning/DTO/create-learning-category.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsString } from "class-validator"; + +import { LearningMessage } from "../../../common/enums/message.enum"; + +export class CreateLearningCategoryDto { + @ApiProperty({ description: "name of the learning category", maxLength: 200 }) + @IsString({ message: LearningMessage.TITLE_STRING }) + @IsNotEmpty({ message: LearningMessage.TITLE_REQUIRED }) + name: string; +} diff --git a/src/modules/learning/DTO/create-learning.dto.ts b/src/modules/learning/DTO/create-learning.dto.ts new file mode 100644 index 0000000..0bc4b8a --- /dev/null +++ b/src/modules/learning/DTO/create-learning.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator"; + +import { LearningMessage } from "../../../common/enums/message.enum"; + +export class CreateLearningDto { + @ApiProperty({ description: "Title of the learning", maxLength: 200 }) + @IsString({ message: LearningMessage.TITLE_STRING }) + @IsNotEmpty({ message: LearningMessage.TITLE_REQUIRED }) + title: string; + + @ApiProperty({ description: "Description of the learning" }) + @IsString({ message: LearningMessage.DESCRIPTION_STRING }) + @IsNotEmpty({ message: LearningMessage.DESCRIPTION_REQUIRED }) + description: string; + + @ApiProperty({ description: "Cover image URL" }) + @IsString({ message: LearningMessage.COVER_URL_INVALID }) + @IsNotEmpty({ message: LearningMessage.COVER_URL_REQUIRED }) + coverUrl: string; + + @ApiProperty({ description: "Video URL", required: false }) + @IsString({ message: LearningMessage.VIDEO_URL_INVALID }) + @IsOptional() + videoUrl?: string; + + @ApiProperty({ description: "Video Duration", required: false }) + @IsString({ message: LearningMessage.VIDEO_URL_INVALID }) + @IsOptional() + videoDuration?: string; + + @IsOptional() + @IsNotEmpty({ message: LearningMessage.CATEGORY_MUST_BE_ID }) + @IsUUID("4", { message: LearningMessage.CATEGORY_MUST_BE_UUID }) + @ApiProperty({ + description: "Category ID", + example: "d290f1ee-6c54-4b01-90e6-d701748f0851", + }) + categoryId: string; +} diff --git a/src/modules/learning/entities/learning-category.entity.ts b/src/modules/learning/entities/learning-category.entity.ts new file mode 100644 index 0000000..cc2da5d --- /dev/null +++ b/src/modules/learning/entities/learning-category.entity.ts @@ -0,0 +1,13 @@ +import { Column, Entity, OneToMany } from "typeorm"; + +import { Learning } from "./learning.entity"; +import { BaseEntity } from "../../../common/entities/base.entity"; + +@Entity() +export class LearningCategory extends BaseEntity { + @Column({ type: "varchar", length: 100 }) + name: string; + + @OneToMany(() => Learning, (learning) => learning.category) + learnings: Learning[]; +} diff --git a/src/modules/learning/entities/learning-progress.entity.ts b/src/modules/learning/entities/learning-progress.entity.ts new file mode 100644 index 0000000..5348c4c --- /dev/null +++ b/src/modules/learning/entities/learning-progress.entity.ts @@ -0,0 +1,17 @@ +import { Column, Entity, ManyToOne } from "typeorm"; + +import { Learning } from "./learning.entity"; +import { BaseEntity } from "../../../common/entities/base.entity"; +import { User } from "../../users/entities/user.entity"; + +@Entity() +export class LearningProgress extends BaseEntity { + @ManyToOne(() => User, (user) => user.learningProgress) + user: User; + + @ManyToOne(() => Learning, (learning) => learning.learningProgress) + learning: Learning; + + @Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" }) + watchedAt: Date; +} diff --git a/src/modules/learning/entities/learning.entity.ts b/src/modules/learning/entities/learning.entity.ts new file mode 100644 index 0000000..fb1ed14 --- /dev/null +++ b/src/modules/learning/entities/learning.entity.ts @@ -0,0 +1,29 @@ +import { Column, Entity, ManyToOne, OneToMany } from "typeorm"; + +import { LearningCategory } from "./learning-category.entity"; +import { LearningProgress } from "./learning-progress.entity"; +import { BaseEntity } from "../../../common/entities/base.entity"; + +@Entity() +export class Learning extends BaseEntity { + @Column({ type: "varchar", length: 200 }) + title: string; + + @Column({ type: "text" }) + description: string; + + @Column({ type: "text" }) + coverUrl: string; + + @Column({ type: "text", nullable: true }) + videoUrl: string; + + @Column({ type: "varchar", length: 200, nullable: true }) + videoDuration: string; + + @ManyToOne(() => LearningCategory, (category) => category.learnings, { eager: true }) + category: LearningCategory; + + @OneToMany(() => LearningProgress, (learningProgress) => learningProgress.learning) + learningProgress: LearningProgress[]; +} diff --git a/src/modules/learning/learning.controller.ts b/src/modules/learning/learning.controller.ts new file mode 100644 index 0000000..3db3248 --- /dev/null +++ b/src/modules/learning/learning.controller.ts @@ -0,0 +1,73 @@ +import { Body, Controller, Get, Param, Post } from "@nestjs/common"; +import { ApiOperation, ApiTags } from "@nestjs/swagger"; + +import { CreateLearningCategoryDto } from "./DTO/create-learning-category.dto"; +import { CreateLearningDto } from "./DTO/create-learning.dto"; +import { LearningService } from "./providers/learning.service"; +import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; +import { Roles } from "../../common/decorators/roles.decorator"; +import { ParamDto } from "../../common/DTO/param.dto"; +import { RoleEnum } from "../users/enums/role.enum"; + +@ApiTags("Learning") +@AuthGuards() +@Controller("learnings") +export class LearningController { + constructor(private readonly learningService: LearningService) {} + + //************************ */ + + @ApiOperation({ summary: "Create learning" }) + @Roles(RoleEnum.ADMIN) + @Post() + create(@Body() createDto: CreateLearningDto) { + return this.learningService.createLearning(createDto); + } + + //************************ */ + + @ApiOperation({ summary: "Create learning category" }) + @Roles(RoleEnum.ADMIN) + @Post("category") + createCategory(@Body() createDto: CreateLearningCategoryDto) { + return this.learningService.createLearningCategory(createDto); + } + + //************************ */ + + @ApiOperation({ summary: "find all learnings" }) + @Roles(RoleEnum.ADMIN, RoleEnum.USER) + @Get() + findAllLearnings() { + return this.learningService.findAllLearnings(); + } + + //************************ */ + + @ApiOperation({ summary: "find one learning by id" }) + @Roles(RoleEnum.ADMIN, RoleEnum.USER) + @Get(":id") + findOneLearning(@Param() paramDto: ParamDto) { + return this.learningService.findOneLearning(paramDto.id); + } + + //************************ */ + + @ApiOperation({ summary: "find all learning category" }) + @Roles(RoleEnum.ADMIN, RoleEnum.USER) + @Get("category") + findAllCategories() { + return this.learningService.findAllCategories(); + } + + //************************ */ + + @ApiOperation({ summary: "find one learning category by id" }) + @Roles(RoleEnum.ADMIN, RoleEnum.USER) + @Get("category/:id") + findOneCategory(@Param() paramDto: ParamDto) { + return this.learningService.findOneCategory(paramDto.id); + } + + //************************ */ +} diff --git a/src/modules/learning/learning.module.ts b/src/modules/learning/learning.module.ts new file mode 100644 index 0000000..99a7864 --- /dev/null +++ b/src/modules/learning/learning.module.ts @@ -0,0 +1,20 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; + +import { LearningCategory } from "./entities/learning-category.entity"; +import { LearningProgress } from "./entities/learning-progress.entity"; +import { Learning } from "./entities/learning.entity"; +import { LearningController } from "./learning.controller"; +import { LearningService } from "./providers/learning.service"; +import { LearningCategoryRepository } from "./repositories/learning-category.repository"; +import { LearningProgressRepository } from "./repositories/learning-progress.repository"; +import { LearningRepository } from "./repositories/learning.repository"; +import { UsersModule } from "../users/users.module"; + +@Module({ + imports: [TypeOrmModule.forFeature([Learning, LearningCategory, LearningProgress]), UsersModule], + providers: [LearningService, LearningRepository, LearningCategoryRepository, LearningProgressRepository], + controllers: [LearningController], + exports: [], +}) +export class LearningModule {} diff --git a/src/modules/learning/providers/learning.service.ts b/src/modules/learning/providers/learning.service.ts new file mode 100644 index 0000000..33f27bc --- /dev/null +++ b/src/modules/learning/providers/learning.service.ts @@ -0,0 +1,106 @@ +import { BadRequestException, Injectable } from "@nestjs/common"; + +import { CommonMessage, LearningMessage } from "../../../common/enums/message.enum"; +import { UsersService } from "../../users/providers/users.service"; +import { CreateLearningCategoryDto } from "../DTO/create-learning-category.dto"; +import { CreateLearningDto } from "../DTO/create-learning.dto"; +import { LearningCategoryRepository } from "../repositories/learning-category.repository"; +import { LearningProgressRepository } from "../repositories/learning-progress.repository"; +import { LearningRepository } from "../repositories/learning.repository"; + +@Injectable() +export class LearningService { + constructor( + private readonly learningRepository: LearningRepository, + private readonly learningProgressRepository: LearningProgressRepository, + private readonly learningCategoryRepository: LearningCategoryRepository, + private readonly usersService: UsersService, + ) {} + + //******************** */ + + async createLearning(createDto: CreateLearningDto) { + const { category } = await this.findOneCategory(createDto.categoryId); + + const learning = this.learningRepository.create({ + ...createDto, + category, + }); + await this.learningRepository.save(learning); + + return { + message: CommonMessage.CREATED, + learning, + }; + } + + //******************** */ + + async findAllLearnings() { + const learnings = await this.learningRepository.find(); + + return { learnings }; + } + + //******************** */ + + async findOneLearning(id: string) { + const learning = await this.learningRepository.findOneBy({ + id, + }); + if (!learning) throw new BadRequestException(LearningMessage.NOT_FOUND); + + return { learning }; + } + + //******************** */ + + async createLearningCategory(createDto: CreateLearningCategoryDto) { + const category = this.learningCategoryRepository.create(createDto); + await this.learningCategoryRepository.save(category); + + return { category }; + } + + //******************** */ + + async findAllCategories() { + const categories = await this.learningCategoryRepository.find(); + + return { categories }; + } + + //******************** */ + + async findOneCategory(id: string) { + const category = await this.learningCategoryRepository.findOneBy({ + id, + }); + if (!category) throw new BadRequestException(LearningMessage.CATEGORY_NOT_FOUND); + + return { category }; + } + + //******************** */ + + async trackProgress(userId: string, learningId: string) { + const { user } = await this.usersService.findOneById(userId); + const { learning } = await this.findOneLearning(learningId); + + const progress = this.learningProgressRepository.create({ + user, + learning, + watchedAt: new Date(), + }); + return this.learningProgressRepository.save(progress); + } + + //******************** */ + + async getUserProgress(userId: string) { + return this.learningProgressRepository.find({ + where: { user: { id: userId } }, + relations: ["learning"], + }); + } +} diff --git a/src/modules/learning/repositories/learning-category.repository.ts b/src/modules/learning/repositories/learning-category.repository.ts new file mode 100644 index 0000000..dc3f43b --- /dev/null +++ b/src/modules/learning/repositories/learning-category.repository.ts @@ -0,0 +1,12 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; + +import { LearningCategory } from "../entities/learning-category.entity"; + +@Injectable() +export class LearningCategoryRepository extends Repository { + constructor(@InjectRepository(LearningCategory) learningCategoryRepository: Repository) { + super(learningCategoryRepository.target, learningCategoryRepository.manager, learningCategoryRepository.queryRunner); + } +} diff --git a/src/modules/learning/repositories/learning-progress.repository.ts b/src/modules/learning/repositories/learning-progress.repository.ts new file mode 100644 index 0000000..a81ca01 --- /dev/null +++ b/src/modules/learning/repositories/learning-progress.repository.ts @@ -0,0 +1,12 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; + +import { LearningProgress } from "../entities/learning-progress.entity"; + +@Injectable() +export class LearningProgressRepository extends Repository { + constructor(@InjectRepository(LearningProgress) learningProgressRepository: Repository) { + super(learningProgressRepository.target, learningProgressRepository.manager, learningProgressRepository.queryRunner); + } +} diff --git a/src/modules/learning/repositories/learning.repository.ts b/src/modules/learning/repositories/learning.repository.ts new file mode 100644 index 0000000..574de9d --- /dev/null +++ b/src/modules/learning/repositories/learning.repository.ts @@ -0,0 +1,12 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; + +import { Learning } from "../entities/learning.entity"; + +@Injectable() +export class LearningRepository extends Repository { + constructor(@InjectRepository(Learning) learningRepository: Repository) { + super(learningRepository.target, learningRepository.manager, learningRepository.queryRunner); + } +} diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 033e945..65f3e58 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -7,6 +7,7 @@ import { BaseEntity } from "../../../common/entities/base.entity"; import { UserAnnouncement } from "../../announcements/entities/user-announcement.entity"; import { Criticism } from "../../criticisms/entities/criticism.entity"; import { Invoice } from "../../invoices/entities/invoice.entity"; +import { LearningProgress } from "../../learning/entities/learning-progress.entity"; import { Notification } from "../../notifications/entities/notification.entity"; import { Payment } from "../../payments/entities/payment.entity"; import { UserSetting } from "../../settings/entities/user-setting.entity"; @@ -78,6 +79,9 @@ export class User extends BaseEntity { @OneToMany(() => Invoice, (invoice) => invoice.user) invoices: Invoice[]; + + @OneToMany(() => LearningProgress, (learningProgress) => learningProgress.learning) + learningProgress: LearningProgress[]; } // @ManyToMany(() => DanakService, (danakService) => danakService.users)