From 5b635ed164ae1a86cb3ab235a6191fb6be0a986b Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Tue, 16 Nov 2021 22:47:26 +0300 Subject: [PATCH] feat: attaching materials to lesson --- .env.example | 4 +-- .../Http/Api/v1/LessonsController.ts | 2 +- app/Models/Lesson.ts | 4 +-- app/Models/LessonContent.ts | 8 +++-- app/Models/LessonMaterial.ts | 25 ++++++++++++++ app/Repositories/LessonRepository.ts | 33 +++++++++++++++++-- app/Services/LessonService.ts | 24 +++++++------- .../Lesson/CreateLessonValidator.ts | 6 ++++ database/factories/index.ts | 6 ++-- database/migrations/1626545059988_images.ts | 2 +- database/migrations/1626964029348_roles.ts | 2 +- .../migrations/1626970448054_users_roles.ts | 2 +- database/migrations/1627022639849_contacts.ts | 2 +- .../migrations/1627750162275_users_courses.ts | 2 +- .../1628017319337_lesson_contents.ts | 2 +- .../migrations/1628622517837_courses_likes.ts | 2 +- .../1637080400230_lesson_materials.ts | 21 ++++++++++++ start/bouncer.ts | 10 ++++++ 18 files changed, 127 insertions(+), 30 deletions(-) create mode 100644 app/Models/LessonMaterial.ts create mode 100644 database/migrations/1637080400230_lesson_materials.ts diff --git a/.env.example b/.env.example index 44fe08c..84c72fe 100644 --- a/.env.example +++ b/.env.example @@ -5,9 +5,9 @@ APP_KEY=IP387l45rlNO3t7Qgjd89qr-a2G4oUgz DB_CONNECTION=pg PG_HOST=localhost PG_PORT=5432 -PG_USER=lucid +PG_USER=postgres PG_PASSWORD= -PG_DB_NAME=lucid +PG_DB_NAME=educt REDIS_CONNECTION=local REDIS_HOST=127.0.0.1 REDIS_PORT=6379 diff --git a/app/Controllers/Http/Api/v1/LessonsController.ts b/app/Controllers/Http/Api/v1/LessonsController.ts index 025fbfb..1cd2e82 100644 --- a/app/Controllers/Http/Api/v1/LessonsController.ts +++ b/app/Controllers/Http/Api/v1/LessonsController.ts @@ -100,7 +100,7 @@ export default class LessonsController extends BaseController { * GET /lessons/:id/content */ 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) { throw new Exception(result.message, result.status, result.error.code); diff --git a/app/Models/Lesson.ts b/app/Models/Lesson.ts index ca29d85..686db69 100644 --- a/app/Models/Lesson.ts +++ b/app/Models/Lesson.ts @@ -28,10 +28,10 @@ export default class Lesson extends BaseModel { }) public content: HasOne; - @column.dateTime({ autoCreate: true, serializeAs: null }) + @column.dateTime({ autoCreate: true }) public createdAt: DateTime; - @column.dateTime({ autoCreate: true, autoUpdate: true, serializeAs: null }) + @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime; /** diff --git a/app/Models/LessonContent.ts b/app/Models/LessonContent.ts index 940e66e..8cd7dc2 100644 --- a/app/Models/LessonContent.ts +++ b/app/Models/LessonContent.ts @@ -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 LessonMaterial from './LessonMaterial'; export default class LessonContent extends BaseModel { @column({ isPrimary: true }) - public id: string; + public id: number; @column() public lesson_id: string; @@ -11,6 +12,9 @@ export default class LessonContent extends BaseModel { @column() public video_url: string; + @hasMany(() => LessonMaterial, { foreignKey: 'lesson_content_id' }) + public materials: HasMany; + @column.dateTime({ autoCreate: true, serializeAs: null }) public createdAt: DateTime; diff --git a/app/Models/LessonMaterial.ts b/app/Models/LessonMaterial.ts new file mode 100644 index 0000000..c0c01a1 --- /dev/null +++ b/app/Models/LessonMaterial.ts @@ -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; +} diff --git a/app/Repositories/LessonRepository.ts b/app/Repositories/LessonRepository.ts index 82e0293..33bd427 100644 --- a/app/Repositories/LessonRepository.ts +++ b/app/Repositories/LessonRepository.ts @@ -61,9 +61,38 @@ export default class LessonRepository { description: data.description, }); + /** + * Attach lesson to 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; } diff --git a/app/Services/LessonService.ts b/app/Services/LessonService.ts index ffbcc7c..5446e0e 100644 --- a/app/Services/LessonService.ts +++ b/app/Services/LessonService.ts @@ -1,11 +1,14 @@ 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 */ import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum'; +// import RoleEnum from 'App/Datatypes/Enums/RoleEnum'; import IResponse from 'App/Datatypes/Interfaces/IResponse'; +// import RoleHelper from 'App/Helpers/RoleHelper'; /** * Repositories */ @@ -171,10 +174,7 @@ export default class LessonService { * @param auth AuthContract * @returns Response */ - public async fetchLessonContent(id: string | number, auth: AuthContract): Promise { - const user = await auth.use('api').authenticate(); - await user.load('courses'); - + public async fetchLessonContent(id: string | number, ctx: HttpContextContract): Promise { const lesson = await this.lessonRepository.getById(id); if (!lesson) { @@ -189,11 +189,10 @@ export default class LessonService { }; } - await lesson.load('course'); - - const userHasCourse = user.courses.find(course => course.id === lesson.course.id); - - if (!userHasCourse) { + /** + * Allow user to view lesson content + */ + if (await ctx.bouncer.denies('viewLessonContent', lesson)) { return { success: false, 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 { success: true, diff --git a/app/Validators/Lesson/CreateLessonValidator.ts b/app/Validators/Lesson/CreateLessonValidator.ts index 55d67c1..c50fb5c 100644 --- a/app/Validators/Lesson/CreateLessonValidator.ts +++ b/app/Validators/Lesson/CreateLessonValidator.ts @@ -9,6 +9,12 @@ export default class CreateLessonValidator { title: schema.string({}, [rules.maxLength(255)]), description: schema.string(), video_url: schema.string({}, [rules.url()]), + materials: schema.array.optional().members( + schema.file({ + size: '10mb', + extnames: ['pdf', 'zip', 'rar', 'doc', 'docx'], + }) + ), }); public messages = {}; diff --git a/database/factories/index.ts b/database/factories/index.ts index 4cf77fc..feef999 100644 --- a/database/factories/index.ts +++ b/database/factories/index.ts @@ -20,7 +20,7 @@ const makeFakeUser = (faker: Faker.FakerStatic, loginPrefix?: string) => { return { first_name: faker.name.firstName(), last_name: faker.name.lastName(), - login: `${loginPrefix || 'user'}${faker.datatype.number(1000)}`, + login: `${loginPrefix || 'user'}${faker.datatype.number(10000)}`, email: faker.internet.exampleEmail(), password: '123456', }; @@ -76,8 +76,8 @@ export const CategoryFactory = Factory.define(Category, ({ faker }) => { export const CourseFactory = Factory.define(Course, ({ faker }) => { return { - title: `Course #${faker.datatype.number(200)}`, - description: faker.lorem.sentence(5), + title: faker.lorem.sentence(8), + description: faker.lorem.sentence(20), status: CourseStatusEnum.PUBLISHED, }; }) diff --git a/database/migrations/1626545059988_images.ts b/database/migrations/1626545059988_images.ts index 4799319..04e5467 100644 --- a/database/migrations/1626545059988_images.ts +++ b/database/migrations/1626545059988_images.ts @@ -5,7 +5,7 @@ export default class Images extends BaseSchema { public async up() { this.schema.createTable(this.tableName, table => { - table.increments('id'); + table.increments('id').primary(); table.string('path').notNullable(); table.string('name').notNullable(); table.string('ext').notNullable(); diff --git a/database/migrations/1626964029348_roles.ts b/database/migrations/1626964029348_roles.ts index 12e78df..005a3fe 100644 --- a/database/migrations/1626964029348_roles.ts +++ b/database/migrations/1626964029348_roles.ts @@ -5,7 +5,7 @@ export default class Roles extends BaseSchema { public async up() { this.schema.createTable(this.tableName, table => { - table.increments('id'); + table.increments('id').primary(); table.string('name').notNullable(); table.string('slug').unique().notNullable(); table.timestamp('created_at', { useTz: true }); diff --git a/database/migrations/1626970448054_users_roles.ts b/database/migrations/1626970448054_users_roles.ts index a95c131..97cc7b9 100644 --- a/database/migrations/1626970448054_users_roles.ts +++ b/database/migrations/1626970448054_users_roles.ts @@ -5,7 +5,7 @@ export default class UsersRoles extends BaseSchema { public async up() { 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.integer('role_id').unsigned().references('roles.id').onDelete('CASCADE'); table.unique(['user_id', 'role_id']); diff --git a/database/migrations/1627022639849_contacts.ts b/database/migrations/1627022639849_contacts.ts index 6f4fa41..f718c11 100644 --- a/database/migrations/1627022639849_contacts.ts +++ b/database/migrations/1627022639849_contacts.ts @@ -5,7 +5,7 @@ export default class Contacts extends BaseSchema { public async up() { 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('phone_number').unique(); table.string('vk_id').unique(); diff --git a/database/migrations/1627750162275_users_courses.ts b/database/migrations/1627750162275_users_courses.ts index 251bced..3a7cb7b 100644 --- a/database/migrations/1627750162275_users_courses.ts +++ b/database/migrations/1627750162275_users_courses.ts @@ -5,7 +5,7 @@ export default class UsersCourses extends BaseSchema { public async up() { 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('course_id', 21).unsigned().references('courses.id').onDelete('CASCADE'); table.unique(['user_id', 'course_id']); diff --git a/database/migrations/1628017319337_lesson_contents.ts b/database/migrations/1628017319337_lesson_contents.ts index ea5c422..569f178 100644 --- a/database/migrations/1628017319337_lesson_contents.ts +++ b/database/migrations/1628017319337_lesson_contents.ts @@ -5,7 +5,7 @@ export default class LessonContents extends BaseSchema { public async up() { 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('video_url').notNullable(); table.timestamp('created_at', { useTz: true }); diff --git a/database/migrations/1628622517837_courses_likes.ts b/database/migrations/1628622517837_courses_likes.ts index 51f177a..2dce802 100644 --- a/database/migrations/1628622517837_courses_likes.ts +++ b/database/migrations/1628622517837_courses_likes.ts @@ -5,7 +5,7 @@ export default class CoursesLikes extends BaseSchema { public async up() { 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('course_id', 21).unsigned().references('courses.id').onDelete('CASCADE'); table.timestamp('liked_on', { useTz: true }); diff --git a/database/migrations/1637080400230_lesson_materials.ts b/database/migrations/1637080400230_lesson_materials.ts new file mode 100644 index 0000000..dbf2eda --- /dev/null +++ b/database/migrations/1637080400230_lesson_materials.ts @@ -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); + } +} diff --git a/start/bouncer.ts b/start/bouncer.ts index 78c32bd..6493865 100644 --- a/start/bouncer.ts +++ b/start/bouncer.ts @@ -8,6 +8,7 @@ import Bouncer from '@ioc:Adonis/Addons/Bouncer'; import RoleEnum from 'App/Datatypes/Enums/RoleEnum'; import RoleHelper from 'App/Helpers/RoleHelper'; +import Lesson from 'App/Models/Lesson'; import Role from 'App/Models/Role'; 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) && !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; }); /*