mirror of
https://github.com/sergeyyarkov/educt-server.git
synced 2026-10-11 10:19:20 +03:00
feat: attaching materials to lesson
This commit is contained in:
parent
d62bccc9dd
commit
5b635ed164
@ -5,9 +5,9 @@ APP_KEY=IP387l45rlNO3t7Qgjd89qr-a2G4oUgz
|
|||||||
DB_CONNECTION=pg
|
DB_CONNECTION=pg
|
||||||
PG_HOST=localhost
|
PG_HOST=localhost
|
||||||
PG_PORT=5432
|
PG_PORT=5432
|
||||||
PG_USER=lucid
|
PG_USER=postgres
|
||||||
PG_PASSWORD=
|
PG_PASSWORD=
|
||||||
PG_DB_NAME=lucid
|
PG_DB_NAME=educt
|
||||||
REDIS_CONNECTION=local
|
REDIS_CONNECTION=local
|
||||||
REDIS_HOST=127.0.0.1
|
REDIS_HOST=127.0.0.1
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|||||||
@ -100,7 +100,7 @@ export default class LessonsController extends BaseController {
|
|||||||
* GET /lessons/:id/content
|
* GET /lessons/:id/content
|
||||||
*/
|
*/
|
||||||
public async getContent(ctx: HttpContextContract) {
|
public async getContent(ctx: HttpContextContract) {
|
||||||
const result = await this.lessonService.fetchLessonContent(ctx.params.id, ctx.auth);
|
const result = await this.lessonService.fetchLessonContent(ctx.params.id, ctx);
|
||||||
|
|
||||||
if (!result.success && result.error) {
|
if (!result.success && result.error) {
|
||||||
throw new Exception(result.message, result.status, result.error.code);
|
throw new Exception(result.message, result.status, result.error.code);
|
||||||
|
|||||||
@ -28,10 +28,10 @@ export default class Lesson extends BaseModel {
|
|||||||
})
|
})
|
||||||
public content: HasOne<typeof LessonContent>;
|
public content: HasOne<typeof LessonContent>;
|
||||||
|
|
||||||
@column.dateTime({ autoCreate: true, serializeAs: null })
|
@column.dateTime({ autoCreate: true })
|
||||||
public createdAt: DateTime;
|
public createdAt: DateTime;
|
||||||
|
|
||||||
@column.dateTime({ autoCreate: true, autoUpdate: true, serializeAs: null })
|
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||||
public updatedAt: DateTime;
|
public updatedAt: DateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm';
|
import { BaseModel, column, HasMany, hasMany } from '@ioc:Adonis/Lucid/Orm';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import LessonMaterial from './LessonMaterial';
|
||||||
|
|
||||||
export default class LessonContent extends BaseModel {
|
export default class LessonContent extends BaseModel {
|
||||||
@column({ isPrimary: true })
|
@column({ isPrimary: true })
|
||||||
public id: string;
|
public id: number;
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
public lesson_id: string;
|
public lesson_id: string;
|
||||||
@ -11,6 +12,9 @@ export default class LessonContent extends BaseModel {
|
|||||||
@column()
|
@column()
|
||||||
public video_url: string;
|
public video_url: string;
|
||||||
|
|
||||||
|
@hasMany(() => LessonMaterial, { foreignKey: 'lesson_content_id' })
|
||||||
|
public materials: HasMany<typeof LessonMaterial>;
|
||||||
|
|
||||||
@column.dateTime({ autoCreate: true, serializeAs: null })
|
@column.dateTime({ autoCreate: true, serializeAs: null })
|
||||||
public createdAt: DateTime;
|
public createdAt: DateTime;
|
||||||
|
|
||||||
|
|||||||
25
app/Models/LessonMaterial.ts
Normal file
25
app/Models/LessonMaterial.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
export default class LessonMaterial extends BaseModel {
|
||||||
|
@column({ isPrimary: true })
|
||||||
|
public id: number;
|
||||||
|
|
||||||
|
@column()
|
||||||
|
public url: string;
|
||||||
|
|
||||||
|
@column()
|
||||||
|
public name: string;
|
||||||
|
|
||||||
|
@column()
|
||||||
|
public ext: string;
|
||||||
|
|
||||||
|
@column()
|
||||||
|
public lesson_content_id: number;
|
||||||
|
|
||||||
|
@column.dateTime({ autoCreate: true })
|
||||||
|
public createdAt: DateTime;
|
||||||
|
|
||||||
|
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||||
|
public updatedAt: DateTime;
|
||||||
|
}
|
||||||
@ -61,9 +61,38 @@ export default class LessonRepository {
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach lesson to course
|
||||||
|
*/
|
||||||
await lesson.related('course').associate(course);
|
await lesson.related('course').associate(course);
|
||||||
await lesson.related('content').create({ video_url: data.video_url });
|
|
||||||
await lesson.load('content');
|
/**
|
||||||
|
* Create content and materials fields in database
|
||||||
|
*/
|
||||||
|
const content = await lesson.related('content').create({ video_url: data.video_url });
|
||||||
|
|
||||||
|
if (data.materials) {
|
||||||
|
await Promise.all(
|
||||||
|
data.materials.map(async file => {
|
||||||
|
/**
|
||||||
|
* Move each file to disk and then save in database
|
||||||
|
*/
|
||||||
|
await file.moveToDisk('materials');
|
||||||
|
if (file.state === 'moved') {
|
||||||
|
await content.related('materials').create({
|
||||||
|
name: file.clientName,
|
||||||
|
ext: file.extname,
|
||||||
|
url: file.filePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load data
|
||||||
|
*/
|
||||||
|
await lesson.load('content', q => q.preload('materials'));
|
||||||
|
|
||||||
return lesson;
|
return lesson;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
import { inject, Ioc } from '@adonisjs/core/build/standalone';
|
import { inject, Ioc } from '@adonisjs/core/build/standalone';
|
||||||
import { AuthContract } from '@ioc:Adonis/Addons/Auth';
|
// import { AuthContract } from '@ioc:Adonis/Addons/Auth';
|
||||||
|
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Datatypes
|
* Datatypes
|
||||||
*/
|
*/
|
||||||
import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum';
|
import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum';
|
||||||
|
// import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
||||||
import IResponse from 'App/Datatypes/Interfaces/IResponse';
|
import IResponse from 'App/Datatypes/Interfaces/IResponse';
|
||||||
|
// import RoleHelper from 'App/Helpers/RoleHelper';
|
||||||
/**
|
/**
|
||||||
* Repositories
|
* Repositories
|
||||||
*/
|
*/
|
||||||
@ -171,10 +174,7 @@ export default class LessonService {
|
|||||||
* @param auth AuthContract
|
* @param auth AuthContract
|
||||||
* @returns Response
|
* @returns Response
|
||||||
*/
|
*/
|
||||||
public async fetchLessonContent(id: string | number, auth: AuthContract): Promise<IResponse> {
|
public async fetchLessonContent(id: string | number, ctx: HttpContextContract): Promise<IResponse> {
|
||||||
const user = await auth.use('api').authenticate();
|
|
||||||
await user.load('courses');
|
|
||||||
|
|
||||||
const lesson = await this.lessonRepository.getById(id);
|
const lesson = await this.lessonRepository.getById(id);
|
||||||
|
|
||||||
if (!lesson) {
|
if (!lesson) {
|
||||||
@ -189,11 +189,10 @@ export default class LessonService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await lesson.load('course');
|
/**
|
||||||
|
* Allow user to view lesson content
|
||||||
const userHasCourse = user.courses.find(course => course.id === lesson.course.id);
|
*/
|
||||||
|
if (await ctx.bouncer.denies('viewLessonContent', lesson)) {
|
||||||
if (!userHasCourse) {
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
status: HttpStatusEnum.FORBIDDEN,
|
status: HttpStatusEnum.FORBIDDEN,
|
||||||
@ -205,7 +204,10 @@ export default class LessonService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await lesson.load('content');
|
/**
|
||||||
|
* Load lesson content with materials
|
||||||
|
*/
|
||||||
|
await lesson.load('content', q => q.preload('materials'));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@ -9,6 +9,12 @@ export default class CreateLessonValidator {
|
|||||||
title: schema.string({}, [rules.maxLength(255)]),
|
title: schema.string({}, [rules.maxLength(255)]),
|
||||||
description: schema.string(),
|
description: schema.string(),
|
||||||
video_url: schema.string({}, [rules.url()]),
|
video_url: schema.string({}, [rules.url()]),
|
||||||
|
materials: schema.array.optional().members(
|
||||||
|
schema.file({
|
||||||
|
size: '10mb',
|
||||||
|
extnames: ['pdf', 'zip', 'rar', 'doc', 'docx'],
|
||||||
|
})
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
public messages = {};
|
public messages = {};
|
||||||
|
|||||||
@ -20,7 +20,7 @@ const makeFakeUser = (faker: Faker.FakerStatic, loginPrefix?: string) => {
|
|||||||
return {
|
return {
|
||||||
first_name: faker.name.firstName(),
|
first_name: faker.name.firstName(),
|
||||||
last_name: faker.name.lastName(),
|
last_name: faker.name.lastName(),
|
||||||
login: `${loginPrefix || 'user'}${faker.datatype.number(1000)}`,
|
login: `${loginPrefix || 'user'}${faker.datatype.number(10000)}`,
|
||||||
email: faker.internet.exampleEmail(),
|
email: faker.internet.exampleEmail(),
|
||||||
password: '123456',
|
password: '123456',
|
||||||
};
|
};
|
||||||
@ -76,8 +76,8 @@ export const CategoryFactory = Factory.define(Category, ({ faker }) => {
|
|||||||
|
|
||||||
export const CourseFactory = Factory.define(Course, ({ faker }) => {
|
export const CourseFactory = Factory.define(Course, ({ faker }) => {
|
||||||
return {
|
return {
|
||||||
title: `Course #${faker.datatype.number(200)}`,
|
title: faker.lorem.sentence(8),
|
||||||
description: faker.lorem.sentence(5),
|
description: faker.lorem.sentence(20),
|
||||||
status: CourseStatusEnum.PUBLISHED,
|
status: CourseStatusEnum.PUBLISHED,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export default class Images extends BaseSchema {
|
|||||||
|
|
||||||
public async up() {
|
public async up() {
|
||||||
this.schema.createTable(this.tableName, table => {
|
this.schema.createTable(this.tableName, table => {
|
||||||
table.increments('id');
|
table.increments('id').primary();
|
||||||
table.string('path').notNullable();
|
table.string('path').notNullable();
|
||||||
table.string('name').notNullable();
|
table.string('name').notNullable();
|
||||||
table.string('ext').notNullable();
|
table.string('ext').notNullable();
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export default class Roles extends BaseSchema {
|
|||||||
|
|
||||||
public async up() {
|
public async up() {
|
||||||
this.schema.createTable(this.tableName, table => {
|
this.schema.createTable(this.tableName, table => {
|
||||||
table.increments('id');
|
table.increments('id').primary();
|
||||||
table.string('name').notNullable();
|
table.string('name').notNullable();
|
||||||
table.string('slug').unique().notNullable();
|
table.string('slug').unique().notNullable();
|
||||||
table.timestamp('created_at', { useTz: true });
|
table.timestamp('created_at', { useTz: true });
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export default class UsersRoles extends BaseSchema {
|
|||||||
|
|
||||||
public async up() {
|
public async up() {
|
||||||
this.schema.createTable(this.tableName, table => {
|
this.schema.createTable(this.tableName, table => {
|
||||||
table.increments('id');
|
table.increments('id').primary();
|
||||||
table.string('user_id', 21).unsigned().references('users.id').onDelete('CASCADE');
|
table.string('user_id', 21).unsigned().references('users.id').onDelete('CASCADE');
|
||||||
table.integer('role_id').unsigned().references('roles.id').onDelete('CASCADE');
|
table.integer('role_id').unsigned().references('roles.id').onDelete('CASCADE');
|
||||||
table.unique(['user_id', 'role_id']);
|
table.unique(['user_id', 'role_id']);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export default class Contacts extends BaseSchema {
|
|||||||
|
|
||||||
public async up() {
|
public async up() {
|
||||||
this.schema.createTable(this.tableName, table => {
|
this.schema.createTable(this.tableName, table => {
|
||||||
table.increments('id');
|
table.increments('id').primary();
|
||||||
table.string('user_id', 21).unsigned().references('users.id').unique().onDelete('CASCADE');
|
table.string('user_id', 21).unsigned().references('users.id').unique().onDelete('CASCADE');
|
||||||
table.string('phone_number').unique();
|
table.string('phone_number').unique();
|
||||||
table.string('vk_id').unique();
|
table.string('vk_id').unique();
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export default class UsersCourses extends BaseSchema {
|
|||||||
|
|
||||||
public async up() {
|
public async up() {
|
||||||
this.schema.createTable(this.tableName, table => {
|
this.schema.createTable(this.tableName, table => {
|
||||||
table.increments('id');
|
table.increments('id').primary();
|
||||||
table.string('user_id', 21).unsigned().references('users.id').onDelete('CASCADE');
|
table.string('user_id', 21).unsigned().references('users.id').onDelete('CASCADE');
|
||||||
table.string('course_id', 21).unsigned().references('courses.id').onDelete('CASCADE');
|
table.string('course_id', 21).unsigned().references('courses.id').onDelete('CASCADE');
|
||||||
table.unique(['user_id', 'course_id']);
|
table.unique(['user_id', 'course_id']);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export default class LessonContents extends BaseSchema {
|
|||||||
|
|
||||||
public async up() {
|
public async up() {
|
||||||
this.schema.createTable(this.tableName, table => {
|
this.schema.createTable(this.tableName, table => {
|
||||||
table.increments('id');
|
table.increments('id').primary();
|
||||||
table.string('lesson_id', 21).unsigned().references('lessons.id').unique().onDelete('CASCADE');
|
table.string('lesson_id', 21).unsigned().references('lessons.id').unique().onDelete('CASCADE');
|
||||||
table.string('video_url').notNullable();
|
table.string('video_url').notNullable();
|
||||||
table.timestamp('created_at', { useTz: true });
|
table.timestamp('created_at', { useTz: true });
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export default class CoursesLikes extends BaseSchema {
|
|||||||
|
|
||||||
public async up() {
|
public async up() {
|
||||||
this.schema.createTable(this.tableName, table => {
|
this.schema.createTable(this.tableName, table => {
|
||||||
table.increments('id');
|
table.increments('id').primary();
|
||||||
table.string('user_id', 21).unsigned().references('users.id').onDelete('CASCADE');
|
table.string('user_id', 21).unsigned().references('users.id').onDelete('CASCADE');
|
||||||
table.string('course_id', 21).unsigned().references('courses.id').onDelete('CASCADE');
|
table.string('course_id', 21).unsigned().references('courses.id').onDelete('CASCADE');
|
||||||
table.timestamp('liked_on', { useTz: true });
|
table.timestamp('liked_on', { useTz: true });
|
||||||
|
|||||||
21
database/migrations/1637080400230_lesson_materials.ts
Normal file
21
database/migrations/1637080400230_lesson_materials.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import BaseSchema from '@ioc:Adonis/Lucid/Schema';
|
||||||
|
|
||||||
|
export default class LessonMaterials extends BaseSchema {
|
||||||
|
protected tableName = 'lesson_materials';
|
||||||
|
|
||||||
|
public async up() {
|
||||||
|
this.schema.createTable(this.tableName, table => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.string('url').notNullable();
|
||||||
|
table.string('name').notNullable();
|
||||||
|
table.string('ext').notNullable();
|
||||||
|
table.integer('lesson_content_id').unsigned().references('lesson_contents.id').onDelete('CASCADE');
|
||||||
|
table.timestamp('created_at', { useTz: true });
|
||||||
|
table.timestamp('updated_at', { useTz: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down() {
|
||||||
|
this.schema.dropTable(this.tableName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@
|
|||||||
import Bouncer from '@ioc:Adonis/Addons/Bouncer';
|
import Bouncer from '@ioc:Adonis/Addons/Bouncer';
|
||||||
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
||||||
import RoleHelper from 'App/Helpers/RoleHelper';
|
import RoleHelper from 'App/Helpers/RoleHelper';
|
||||||
|
import Lesson from 'App/Models/Lesson';
|
||||||
import Role from 'App/Models/Role';
|
import Role from 'App/Models/Role';
|
||||||
import User from 'App/Models/User';
|
import User from 'App/Models/User';
|
||||||
|
|
||||||
@ -39,6 +40,15 @@ export const { actions } = Bouncer.define('manageUserRole', async (user: User, r
|
|||||||
(role.slug === RoleEnum.ADMIN || role.slug === RoleEnum.TEACHER) &&
|
(role.slug === RoleEnum.ADMIN || role.slug === RoleEnum.TEACHER) &&
|
||||||
!RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN])
|
!RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN])
|
||||||
);
|
);
|
||||||
|
}).define('viewLessonContent', async (user: User, lesson: Lesson) => {
|
||||||
|
await user.load(loader => loader.load('roles').load('courses'));
|
||||||
|
await lesson.load('course');
|
||||||
|
|
||||||
|
if (!RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN, RoleEnum.TEACHER])) {
|
||||||
|
return !!user.courses.find(course => course.id === lesson.course.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user