From a9715971e47720e35da543b641af7ae706c81698 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sat, 31 Jul 2021 21:16:44 +0300 Subject: [PATCH] added students to course --- app/Controllers/Http/CoursesController.ts | 66 ++++++++++++++++++- app/Controllers/Http/UsersController.ts | 4 +- app/Models/Course.ts | 23 +++++-- app/Models/User.ts | 6 ++ app/Validators/AddCourseToUserValidator.ts | 12 ++++ app/Validators/DelCourseFromUserValidator.ts | 12 ++++ .../migrations/1627750162275_users_courses.ts | 18 +++++ start/routes.ts | 10 ++- 8 files changed, 143 insertions(+), 8 deletions(-) create mode 100644 app/Validators/AddCourseToUserValidator.ts create mode 100644 app/Validators/DelCourseFromUserValidator.ts create mode 100644 database/migrations/1627750162275_users_courses.ts diff --git a/app/Controllers/Http/CoursesController.ts b/app/Controllers/Http/CoursesController.ts index ef3a4fd..79d62b2 100644 --- a/app/Controllers/Http/CoursesController.ts +++ b/app/Controllers/Http/CoursesController.ts @@ -2,7 +2,9 @@ import { Exception } from '@adonisjs/core/build/standalone'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import Course from 'App/Models/Course'; import User from 'App/Models/User'; +import AddCourseToUserValidator from 'App/Validators/AddCourseToUserValidator'; import CreateCourseValidator from 'App/Validators/CreateCourseValidator'; +import DelCourseFromUserValidator from 'App/Validators/DelCourseFromUserValidator'; import UpdateCourseValidator from 'App/Validators/UpdateCourseValidator'; export default class CoursesController { @@ -95,8 +97,16 @@ export default class CoursesController { /** * Show Students of Course with "id" + * GET /courses/:id/students */ - public async showStudents({ response, params }: HttpContextContract) {} + public async showStudents({ response, params }: HttpContextContract) { + const course = await this.Course.query().preload('students').where('id', params.id).firstOrFail(); + + return response.ok({ + message: `Fetched students from course with id: "${course.id}"`, + data: course.students, + }); + } /** * Create new Course in a system @@ -178,4 +188,58 @@ export default class CoursesController { data: course, }); } + + /** + * Attach student to course + * POST /courses/:id/attach-student + */ + async attachStudent({ response, request, params }: HttpContextContract) { + try { + const payload = await request.validate(AddCourseToUserValidator); + const student = await this.User.findOrFail(payload.student_id); + const course = await this.Course.findOrFail(params.id); + + await course.load('students'); + await student.load('roles'); + + const studentRoles = student.roles.map(r => r.slug); + + if (!studentRoles.includes('student')) { + throw new Exception(`User cannot be attached to the course without "student" role.`, 400, 'E_USER_ROLE'); + } + + await course.related('students').attach([student.id]); + + return response.ok({ + message: 'Student attached', + data: 'SUCCESS', + }); + } catch (error) { + if (error?.code === '23505') + throw new Exception('Student already attached to that course', 400, 'E_STUDENT_ATTACHED'); + + throw error; + } + } + + /** + * Detach student from course + */ + async detachStudent({ response, request, params }: HttpContextContract) { + const payload = await request.validate(DelCourseFromUserValidator); + const course = await this.Course.query().preload('students').where('id', params.id).firstOrFail(); + const candidate = course.students.find(student => student.id === payload.student_id); + + if (!candidate) { + throw new Exception('Student not attached to that course', 400, 'E_STUDENT_NOT_ATTACHED'); + } + + const student = await this.User.findOrFail(payload.student_id); + await course.related('students').detach([student.id]); + + return response.ok({ + message: 'Student detached', + data: 'SUCCESS', + }); + } } diff --git a/app/Controllers/Http/UsersController.ts b/app/Controllers/Http/UsersController.ts index 5f98dfe..92de751 100644 --- a/app/Controllers/Http/UsersController.ts +++ b/app/Controllers/Http/UsersController.ts @@ -122,7 +122,7 @@ export default class UsersController { /** * Attach array of roles to user - * POST /users/:id/attach_roles + * POST /users/:id/attach-roles */ public async attachRoles({ request, response, params }: HttpContextContract) { await request.validate(AddRoleToUserValidator); @@ -141,7 +141,7 @@ export default class UsersController { /** * Detach array of roles from user - * DELETE /users/:id/detach_roles + * DELETE /users/:id/detach-roles */ public async detachRoles({ request, response, params }: HttpContextContract) { await request.validate(DelRoleFromUserValidator); diff --git a/app/Models/Course.ts b/app/Models/Course.ts index 02dfd1b..6cdeba8 100644 --- a/app/Models/Course.ts +++ b/app/Models/Course.ts @@ -1,10 +1,20 @@ /* eslint-disable no-param-reassign */ import { nanoid } from 'nanoid'; import { DateTime } from 'luxon'; -import { BaseModel, beforeCreate, BelongsTo, belongsTo, column, HasMany, hasMany } from '@ioc:Adonis/Lucid/Orm'; -import Category from './Category'; -import Lesson from './Lesson'; -import User from './User'; +import { + BaseModel, + beforeCreate, + BelongsTo, + belongsTo, + column, + HasMany, + hasMany, + ManyToMany, + manyToMany, +} from '@ioc:Adonis/Lucid/Orm'; +import Category from 'App/Models/Category'; +import Lesson from 'App/Models/Lesson'; +import User from 'App/Models/User'; // eslint-disable-next-line import/no-cycle export default class Course extends BaseModel { @@ -38,6 +48,11 @@ export default class Course extends BaseModel { }) public lessons: HasMany; + @manyToMany(() => User, { + pivotTable: 'users_courses', + }) + public students: ManyToMany; + @column.dateTime({ autoCreate: true }) public createdAt: DateTime; diff --git a/app/Models/User.ts b/app/Models/User.ts index c903fbb..f63ce36 100644 --- a/app/Models/User.ts +++ b/app/Models/User.ts @@ -16,6 +16,7 @@ import Hash from '@ioc:Adonis/Core/Hash'; import { nanoid } from 'nanoid'; import Role from 'App/Models/Role'; import Contact from 'App/Models/Contact'; +import Course from 'App/Models/Course'; export default class User extends BaseModel { @column({ isPrimary: true }) @@ -43,6 +44,11 @@ export default class User extends BaseModel { }) public roles: ManyToMany; + @manyToMany(() => Course, { + pivotTable: 'users_courses', + }) + public courses: ManyToMany; + @column.dateTime({ autoCreate: true, serializeAs: null }) public createdAt: DateTime; diff --git a/app/Validators/AddCourseToUserValidator.ts b/app/Validators/AddCourseToUserValidator.ts new file mode 100644 index 0000000..707f3cb --- /dev/null +++ b/app/Validators/AddCourseToUserValidator.ts @@ -0,0 +1,12 @@ +import { schema, rules } from '@ioc:Adonis/Core/Validator'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; + +export default class AddCourseToUserValidator { + constructor(protected ctx: HttpContextContract) {} + + public schema = schema.create({ + student_id: schema.string({}, [rules.exists({ table: 'users', column: 'id' })]), + }); + + public messages = {}; +} diff --git a/app/Validators/DelCourseFromUserValidator.ts b/app/Validators/DelCourseFromUserValidator.ts new file mode 100644 index 0000000..bb34442 --- /dev/null +++ b/app/Validators/DelCourseFromUserValidator.ts @@ -0,0 +1,12 @@ +import { schema, rules } from '@ioc:Adonis/Core/Validator'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; + +export default class DelCourseFromUserValidator { + constructor(protected ctx: HttpContextContract) {} + + public schema = schema.create({ + student_id: schema.string({}, [rules.exists({ table: 'users', column: 'id' })]), + }); + + public messages = {}; +} diff --git a/database/migrations/1627750162275_users_courses.ts b/database/migrations/1627750162275_users_courses.ts new file mode 100644 index 0000000..251bced --- /dev/null +++ b/database/migrations/1627750162275_users_courses.ts @@ -0,0 +1,18 @@ +import BaseSchema from '@ioc:Adonis/Lucid/Schema'; + +export default class UsersCourses extends BaseSchema { + protected tableName = 'users_courses'; + + 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('course_id', 21).unsigned().references('courses.id').onDelete('CASCADE'); + table.unique(['user_id', 'course_id']); + }); + } + + public async down() { + this.schema.dropTable(this.tableName); + } +} diff --git a/start/routes.ts b/start/routes.ts index ea944d5..5ce9541 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -72,9 +72,17 @@ Route.group(() => { .middleware('role:admin,teacher,student') .as('showCourseLessons'); - Route.get('courses/:id/students', 'CourseController.showStudents') + Route.get('courses/:id/students', 'CoursesController.showStudents') .middleware('role:admin,teacher,student') .as('showCourseStudents'); + + Route.post('courses/:id/attach-student', 'CoursesController.attachStudent') + .middleware('role:admin,teacher') + .as('attachUserCourse'); + + Route.delete('courses/:id/detach-student', 'CoursesController.detachStudent') + .middleware('role:admin,teacher') + .as('detachUserCourse'); }).middleware('auth'); }) .prefix('/api/v1')