From bb513d8dab59167742c08f74feccc10c4aee09c8 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Wed, 22 Dec 2021 21:13:42 +0300 Subject: [PATCH] feat: added lesson progress --- .../Http/Api/v1/CoursesController.ts | 6 +- .../Http/Api/v1/LessonsController.ts | 14 +++++ app/Models/Lesson.ts | 6 ++ app/Models/LessonProgress.ts | 28 +++++++++ app/Models/User.ts | 8 +++ app/Repositories/CourseRepository.ts | 9 ++- app/Repositories/LessonProgressRepository.ts | 23 +++++++ app/Services/CourseService.ts | 9 ++- app/Services/LessonService.ts | 62 ++++++++++++++++++- .../1640112055569_lesson_progresses.ts | 19 ++++++ routes/apis/v1/courses.ts | 4 ++ routes/apis/v1/lessons.ts | 3 + 12 files changed, 181 insertions(+), 10 deletions(-) create mode 100644 app/Models/LessonProgress.ts create mode 100644 app/Repositories/LessonProgressRepository.ts create mode 100644 database/migrations/1640112055569_lesson_progresses.ts diff --git a/app/Controllers/Http/Api/v1/CoursesController.ts b/app/Controllers/Http/Api/v1/CoursesController.ts index 5a82a82..c3bda0e 100644 --- a/app/Controllers/Http/Api/v1/CoursesController.ts +++ b/app/Controllers/Http/Api/v1/CoursesController.ts @@ -73,7 +73,7 @@ export default class CoursesController extends BaseController { } /** - * Show Category of Course with "id" + * Show Category by course id * GET /courses/:id/category */ public async showCategory(ctx: HttpContextContract) { @@ -87,11 +87,11 @@ export default class CoursesController extends BaseController { } /** - * Show Lessons of ourse with "id" + * Show Lessons by course id * GET /courses/:id/lessons */ public async showLessons(ctx: HttpContextContract) { - const result = await this.courseService.fetchCourseLessons(ctx.params.id); + const result = await this.courseService.fetchCourseLessons(ctx.params.id, ctx); if (!result.success && result.error) { throw new Exception(result.message, result.status, result.error.code); diff --git a/app/Controllers/Http/Api/v1/LessonsController.ts b/app/Controllers/Http/Api/v1/LessonsController.ts index ada9a96..010e413 100644 --- a/app/Controllers/Http/Api/v1/LessonsController.ts +++ b/app/Controllers/Http/Api/v1/LessonsController.ts @@ -157,6 +157,20 @@ export default class LessonsController extends BaseController { return this.sendResponse(ctx, data, message, status); } + + /** + * Get information about progress of video + * GET /lessons/:id/progress + */ + public async getVideoProgress(ctx: HttpContextContract) { + const { data, message, status, success, error } = await this.lessonService.fetchLessonProgress(ctx.params.id, ctx); + + if (!success && error) { + throw new Exception(message, status, error.code); + } + + return this.sendResponse(ctx, data, message, status); + } } new Ioc().make(LessonsController); diff --git a/app/Models/Lesson.ts b/app/Models/Lesson.ts index 3fb3dbd..63ae5e0 100644 --- a/app/Models/Lesson.ts +++ b/app/Models/Lesson.ts @@ -18,6 +18,7 @@ import Course from './Course'; import LessonMaterial from './LessonMaterial'; import LessonContent from './LessonContent'; import Color from './Color'; +import LessonProgress from './LessonProgress'; export default class Lesson extends BaseModel { @column({ isPrimary: true }) @@ -59,6 +60,11 @@ export default class Lesson extends BaseModel { @hasMany(() => LessonMaterial, { foreignKey: 'lesson_id' }) public materials: HasMany; + @hasOne(() => LessonProgress, { + foreignKey: 'lesson_id', + }) + public progress: HasOne; + @column.dateTime({ autoCreate: true }) public createdAt: DateTime; diff --git a/app/Models/LessonProgress.ts b/app/Models/LessonProgress.ts new file mode 100644 index 0000000..745e502 --- /dev/null +++ b/app/Models/LessonProgress.ts @@ -0,0 +1,28 @@ +/* eslint-disable import/no-cycle */ +import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm'; +import Lesson from './Lesson'; +import User from './User'; + +export default class LessonProgress extends BaseModel { + @column({ isPrimary: true, serializeAs: null }) + public id: number; + + @column({ serializeAs: null }) + public lesson_id: string; + + @column({ serializeAs: null }) + public user_id: string; + + @column() + public is_watched: boolean; + + @belongsTo(() => Lesson, { + foreignKey: 'lesson_id', + }) + public lesson: BelongsTo; + + @belongsTo(() => User, { + foreignKey: 'user_id', + }) + public user: BelongsTo; +} diff --git a/app/Models/User.ts b/app/Models/User.ts index 4eb9bae..695fa6b 100644 --- a/app/Models/User.ts +++ b/app/Models/User.ts @@ -7,6 +7,8 @@ import { beforeSave, column, computed, + HasMany, + hasMany, HasOne, hasOne, ManyToMany, @@ -17,6 +19,7 @@ import { nanoid } from 'nanoid'; import Role from 'App/Models/Role'; import Contact from 'App/Models/Contact'; import Course from 'App/Models/Course'; +import LessonProgress from './LessonProgress'; export default class User extends BaseModel { @column({ isPrimary: true }) @@ -62,6 +65,11 @@ export default class User extends BaseModel { }) public likes: ManyToMany; + @hasMany(() => LessonProgress, { + foreignKey: 'user_id', + }) + public lessons_progress: HasMany; + @column.dateTime({ autoCreate: true }) public createdAt: DateTime; diff --git a/app/Repositories/CourseRepository.ts b/app/Repositories/CourseRepository.ts index c43fcbd..0581e30 100644 --- a/app/Repositories/CourseRepository.ts +++ b/app/Repositories/CourseRepository.ts @@ -156,10 +156,15 @@ export default class CourseRepository { * @param id Course id * @returns List of lessons or null */ - public async getLessons(id: string | number): Promise { + public async getLessons(id: string | number, userId: string): Promise { const course = await this.Course.query() .select('id') - .preload('lessons', q => q.preload('color').orderBy('display_order', 'asc')) + .preload('lessons', q => + q + .preload('progress', p => p.where('user_id', userId)) + .preload('color') + .orderBy('display_order', 'asc') + ) .where('id', id) .first(); return course && course.lessons; diff --git a/app/Repositories/LessonProgressRepository.ts b/app/Repositories/LessonProgressRepository.ts new file mode 100644 index 0000000..5dfab41 --- /dev/null +++ b/app/Repositories/LessonProgressRepository.ts @@ -0,0 +1,23 @@ +import LessonProgress from 'App/Models/LessonProgress'; + +export default class LessonProgressRepository { + private LessonProgress: typeof LessonProgress; + + constructor() { + this.LessonProgress = LessonProgress; + } + + public async get(user_id: string, lesson_id: string): Promise { + const progress = await this.LessonProgress.query() + .where('user_id', user_id) + .andWhere('lesson_id', lesson_id) + .first(); + + return progress; + } + + public async create(data: { user_id: string; lesson_id: string; is_watched: boolean }): Promise { + const progress = await this.LessonProgress.create(data); + return progress; + } +} diff --git a/app/Services/CourseService.ts b/app/Services/CourseService.ts index e650312..24fcfa7 100644 --- a/app/Services/CourseService.ts +++ b/app/Services/CourseService.ts @@ -1,5 +1,6 @@ import { inject, Ioc } from '@adonisjs/core/build/standalone'; import { AuthContract } from '@ioc:Adonis/Addons/Auth'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; /** * Datatypes @@ -23,6 +24,7 @@ import CategoryRepository from 'App/Repositories/CategoryRepository'; import CreateCourseValidator from 'App/Validators/Course/CreateCourseValidator'; import UpdateCourseValidator from 'App/Validators/Course/UpdateCourseValidator'; import FetchCoursesValidator from 'App/Validators/Course/FetchCoursesValidator'; +import Course from 'App/Models/Course'; @inject() export default class CourseService { @@ -64,7 +66,7 @@ export default class CourseService { * @param id Course id * @returns Response */ - public async fetchCourse(id: string | number): Promise { + public async fetchCourse(id: string | number): Promise> { const data = await this.courseRepository.getById(id); if (!data) { @@ -151,8 +153,9 @@ export default class CourseService { * @param id Course id * @returns Response */ - public async fetchCourseLessons(id: string | number): Promise { - const data = await this.courseRepository.getLessons(id); + public async fetchCourseLessons(id: string | number, ctx: HttpContextContract): Promise { + const user = await ctx.auth.use('api').authenticate(); + const data = await this.courseRepository.getLessons(id, user.id); if (!data) { return { diff --git a/app/Services/LessonService.ts b/app/Services/LessonService.ts index f161322..3eaef25 100644 --- a/app/Services/LessonService.ts +++ b/app/Services/LessonService.ts @@ -13,6 +13,7 @@ import LessonMaterial from 'App/Models/LessonMaterial'; * Repositories */ import CourseRepository from 'App/Repositories/CourseRepository'; +import LessonProgressRepository from 'App/Repositories/LessonProgressRepository'; import LessonRepository from 'App/Repositories/LessonRepository'; /** @@ -27,9 +28,16 @@ export default class LessonService { private lessonRepository: LessonRepository; - constructor(courseRepository: CourseRepository, lessonRepository: LessonRepository) { + private lessonProgressRepository: LessonProgressRepository; + + constructor( + courseRepository: CourseRepository, + lessonRepository: LessonRepository, + lessonProgressRepository: LessonProgressRepository + ) { this.courseRepository = courseRepository; this.lessonRepository = lessonRepository; + this.lessonProgressRepository = lessonProgressRepository; } /** @@ -54,7 +62,7 @@ export default class LessonService { * @param id Lesson id * @returns Response */ - public async fetchLesson(id: string | number, ctx: HttpContextContract): Promise { + public async fetchLesson(id: string | number, ctx: HttpContextContract) { const lesson = await this.lessonRepository.getById(id); if (!lesson) { @@ -143,6 +151,56 @@ export default class LessonService { }; } + /** + * Fetch progress of lesson by lesson id + * + * @param id Lesson id + * @param ctx Http context + */ + public async fetchLessonProgress(id: string, ctx: HttpContextContract): Promise { + const user = await ctx.auth.use('api').authenticate(); + const lesson = await this.lessonRepository.getById(id); + + if (!lesson) { + return { + success: false, + status: HttpStatusEnum.NOT_FOUND, + message: 'Lesson not found.', + data: {}, + error: { + code: 'E_NOT_FOUND', + }, + }; + } + + if (await ctx.bouncer.denies('viewLessonContent', lesson)) { + return { + success: false, + status: HttpStatusEnum.FORBIDDEN, + message: 'The user is not a student of this course.', + data: {}, + error: { + code: 'E_FORBIDDEN', + }, + }; + } + + const progress = await this.lessonProgressRepository.get(user.id, lesson.id); + + if (progress === null) { + await this.lessonProgressRepository.create({ user_id: user.id, lesson_id: lesson.id, is_watched: true }); + } + + return { + success: true, + status: HttpStatusEnum.OK, + message: 'Fetched lesson progress', + data: { + progress, + }, + }; + } + /** * Create lesson * diff --git a/database/migrations/1640112055569_lesson_progresses.ts b/database/migrations/1640112055569_lesson_progresses.ts new file mode 100644 index 0000000..9b3ad8a --- /dev/null +++ b/database/migrations/1640112055569_lesson_progresses.ts @@ -0,0 +1,19 @@ +import BaseSchema from '@ioc:Adonis/Lucid/Schema'; + +export default class LessonProgresses extends BaseSchema { + protected tableName = 'lesson_progresses'; + + public async up() { + this.schema.createTable(this.tableName, table => { + table.increments('id'); + table.string('user_id', 21).unsigned().references('users.id').onDelete('CASCADE'); + table.string('lesson_id', 21).unsigned().references('lessons.id').onDelete('CASCADE'); + table.boolean('is_watched').notNullable(); + table.unique(['user_id', 'lesson_id']); + }); + } + + public async down() { + this.schema.dropTable(this.tableName); + } +} diff --git a/routes/apis/v1/courses.ts b/routes/apis/v1/courses.ts index 319a394..087a483 100644 --- a/routes/apis/v1/courses.ts +++ b/routes/apis/v1/courses.ts @@ -57,4 +57,8 @@ Route.group(() => { Route.post('/:id/set-status', 'Api/v1/CoursesController.setStatus') .middleware('role:admin,teacher') .as('courses.set-status'); + + Route.get('/:id/lessons-progress', 'Api/v1/CoursesController.getLessonsProgress') + .middleware('role:admin,teacher,student') + .as('courses.get-lessons-progress'); }).prefix('courses'); diff --git a/routes/apis/v1/lessons.ts b/routes/apis/v1/lessons.ts index f2fa3f4..b56e8af 100644 --- a/routes/apis/v1/lessons.ts +++ b/routes/apis/v1/lessons.ts @@ -15,4 +15,7 @@ Route.group(() => { Route.get('/materials/:file', 'Api/v1/LessonsController.getMaterial') .middleware('role:admin,teacher,student') .as('lessons.get-material'); + Route.get('/:id/progress', 'Api/v1/LessonsController.getVideoProgress') + .middleware('role:admin,teacher,student') + .as('lessons.get-video-progress'); }).prefix('lessons');