feat: added lesson progress

This commit is contained in:
Sergey Yarkov 2021-12-22 21:13:42 +03:00
parent 14a89f77b3
commit bb513d8dab
12 changed files with 181 additions and 10 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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<typeof LessonMaterial>;
@hasOne(() => LessonProgress, {
foreignKey: 'lesson_id',
})
public progress: HasOne<typeof LessonProgress>;
@column.dateTime({ autoCreate: true })
public createdAt: DateTime;

View File

@ -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<typeof Lesson>;
@belongsTo(() => User, {
foreignKey: 'user_id',
})
public user: BelongsTo<typeof User>;
}

View File

@ -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<typeof Course>;
@hasMany(() => LessonProgress, {
foreignKey: 'user_id',
})
public lessons_progress: HasMany<typeof LessonProgress>;
@column.dateTime({ autoCreate: true })
public createdAt: DateTime;

View File

@ -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<Lesson[] | null> {
public async getLessons(id: string | number, userId: string): Promise<Lesson[] | null> {
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;

View File

@ -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<LessonProgress | null> {
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<LessonProgress> {
const progress = await this.LessonProgress.create(data);
return progress;
}
}

View File

@ -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<IResponse> {
public async fetchCourse(id: string | number): Promise<IResponse<Course | {}>> {
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<IResponse> {
const data = await this.courseRepository.getLessons(id);
public async fetchCourseLessons(id: string | number, ctx: HttpContextContract): Promise<IResponse> {
const user = await ctx.auth.use('api').authenticate();
const data = await this.courseRepository.getLessons(id, user.id);
if (!data) {
return {

View File

@ -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<IResponse> {
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<IResponse> {
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
*

View File

@ -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);
}
}

View File

@ -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');

View File

@ -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');