mirror of
https://github.com/sergeyyarkov/educt-server.git
synced 2026-10-11 18:29:15 +03:00
feat: added lesson progress
This commit is contained in:
parent
14a89f77b3
commit
bb513d8dab
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
28
app/Models/LessonProgress.ts
Normal file
28
app/Models/LessonProgress.ts
Normal 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>;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
23
app/Repositories/LessonProgressRepository.ts
Normal file
23
app/Repositories/LessonProgressRepository.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
*
|
||||
|
||||
19
database/migrations/1640112055569_lesson_progresses.ts
Normal file
19
database/migrations/1640112055569_lesson_progresses.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
|
||||
@ -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');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user