feat: attaching materials to lesson

This commit is contained in:
Sergey Yarkov 2021-11-16 22:47:26 +03:00
parent d62bccc9dd
commit 5b635ed164
18 changed files with 127 additions and 30 deletions

View File

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

View File

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

View File

@ -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;
/** /**

View File

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

View 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;
}

View File

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

View File

@ -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,

View File

@ -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 = {};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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