Merge branch 'feature/sequence-of-lessons' into develop

This commit is contained in:
Sergey Yarkov 2021-11-25 20:35:50 +03:00
commit fe8deb7837
12 changed files with 75 additions and 15 deletions

View File

@ -1,6 +1,7 @@
import Application from '@ioc:Adonis/Core/Application'; import Application from '@ioc:Adonis/Core/Application';
import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone'; import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import { schema } from '@ioc:Adonis/Core/Validator';
/** /**
* Services * Services
@ -137,6 +138,25 @@ export default class LessonsController extends BaseController {
return this.sendResponse(ctx, data, message, status); return this.sendResponse(ctx, data, message, status);
} }
/**
* Save order of lessons
* POST /lessons/save-order
*/
// eslint-disable-next-line class-methods-use-this
public async saveOrder(ctx: HttpContextContract) {
const saveOrderSchema = schema.create({
ids: schema.array().members(schema.string()),
});
const payload = await ctx.request.validate({ schema: saveOrderSchema });
const { data, message, status, success, error } = await this.lessonService.updateOrder(payload.ids);
if (!success && error) {
throw new Exception(message, status, error.code);
}
return this.sendResponse(ctx, data, message, status);
}
} }
new Ioc().make(LessonsController); new Ioc().make(LessonsController);

View File

@ -11,15 +11,6 @@ enum ColorEnum {
Purple = '#800080', Purple = '#800080',
Indigo = '#4B0082', Indigo = '#4B0082',
Teal = '#008080', Teal = '#008080',
LightGray = '#D3D3D3',
LightSlateGray = '#778899',
LightGreen = '#90EE90',
LightSeaGreen = '#20B2AA',
LightCoral = '#FFA07A',
LightCyan = '#E0FFFF',
LightSteelBlue = '#B0C4DE',
LightBlue = '#ADD8E6',
LightSkyBlue = '#87CEFA',
MediumVioletRed = '#C71585', MediumVioletRed = '#C71585',
MediumOrchid = '#BA55D3', MediumOrchid = '#BA55D3',
MediumPurple = '#9370DB', MediumPurple = '#9370DB',

View File

@ -2,6 +2,7 @@
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { BaseModel, beforeCreate, BelongsTo, belongsTo, column, HasOne, hasOne } from '@ioc:Adonis/Lucid/Orm'; import { BaseModel, beforeCreate, BelongsTo, belongsTo, column, HasOne, hasOne } from '@ioc:Adonis/Lucid/Orm';
import ColorHelper from 'App/Helpers/ColorHelper';
import Course from './Course'; import Course from './Course';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import LessonContent from './LessonContent'; import LessonContent from './LessonContent';
@ -17,6 +18,9 @@ export default class Lesson extends BaseModel {
@column() @column()
public title: string; public title: string;
@column({ serializeAs: null })
public display_order: number;
@column() @column()
public description: string; public description: string;
@ -55,4 +59,13 @@ export default class Lesson extends BaseModel {
public static assignLessonId(lesson: Lesson) { public static assignLessonId(lesson: Lesson) {
lesson.id = nanoid(); lesson.id = nanoid();
} }
@beforeCreate()
public static async assignRandomColor(course: Course) {
const color = await ColorHelper.generateRandomColor();
if (color) {
course.color_id = color.id;
}
}
} }

View File

@ -82,7 +82,7 @@ export default class CourseRepository {
.preload('teacher') .preload('teacher')
.preload('color') .preload('color')
.preload('category') .preload('category')
.preload('lessons', q => q.preload('color')) .preload('lessons', q => q.preload('color').orderBy('display_order', 'asc'))
.preload('students') .preload('students')
.where('id', id) .where('id', id)
.first(); .first();

View File

@ -72,10 +72,13 @@ export default class LessonRepository {
* @returns Lesson * @returns Lesson
*/ */
public async create(course: Course, data: CreateLessonValidator['schema']['props']): Promise<Lesson> { public async create(course: Course, data: CreateLessonValidator['schema']['props']): Promise<Lesson> {
await course.loadCount('lessons');
const lesson = await this.Lesson.create({ const lesson = await this.Lesson.create({
title: data.title, title: data.title,
description: data.description, description: data.description,
duration: data.duration.toFormat('HH:mm:ss'), duration: data.duration.toFormat('HH:mm:ss'),
display_order: Number.parseInt(course.$extras.lessons_count, 10) + 1,
}); });
/** /**

View File

@ -1,5 +1,6 @@
import { inject, Ioc } from '@adonisjs/core/build/standalone'; import { inject, Ioc } from '@adonisjs/core/build/standalone';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import Database from '@ioc:Adonis/Lucid/Database';
/** /**
* Datatypes * Datatypes
@ -264,6 +265,32 @@ export default class LessonService {
data: lesson.content, data: lesson.content,
}; };
} }
// eslint-disable-next-line class-methods-use-this
public async updateOrder(ids: string[]): Promise<IResponse<{}>> {
/**
* Update order of lessons
*/
const params = ids
.map((id, i) => ({ id, i: i + 1 }))
.reduce((result: Array<number | string>, { id, i }) => result.concat(id, i), []);
const query = `
UPDATE lessons
SET display_order = t.display_order
FROM (VALUES ${ids.map(() => `(?, ??)`).join(',')}) as t(id, display_order)
WHERE t.id = lessons.id
`;
await Database.rawQuery(query, params);
return {
success: true,
status: HttpStatusEnum.OK,
message: 'Order updated.',
data: {},
};
}
} }
new Ioc().make(LessonService); new Ioc().make(LessonService);

View File

@ -64,12 +64,13 @@ export const LessonFactory = Factory.define(Lesson, ({ faker }) => {
return { return {
title: faker.lorem.sentence(6), title: faker.lorem.sentence(6),
description: faker.lorem.sentence(7), description: faker.lorem.sentence(7),
display_order: 1,
duration: DateTime.fromObject({ duration: DateTime.fromObject({
hour: faker.datatype.number(2), hour: faker.datatype.number(2),
minute: faker.datatype.number({ min: 30, max: 59 }), minute: faker.datatype.number({ min: 30, max: 59 }),
second: 0, second: 0,
}).toFormat('HH:mm:ss'), }).toFormat('HH:mm:ss'),
color_id: faker.datatype.number({ min: 1, max: 25 }), color_id: faker.datatype.number({ min: 1, max: 15 }),
}; };
}) })
.relation('content', () => LessonContentFactory) .relation('content', () => LessonContentFactory)

View File

@ -11,7 +11,7 @@ export default class Colors extends BaseSchema {
.enu('name', Object.keys(ColorEnum), { .enu('name', Object.keys(ColorEnum), {
useNative: true, useNative: true,
enumName: 'color_name_enum', enumName: 'color_name_enum',
existingType: true, existingType: false,
schemaName: 'public', schemaName: 'public',
}) })
.notNullable(); .notNullable();
@ -20,6 +20,7 @@ export default class Colors extends BaseSchema {
} }
public async down() { public async down() {
await this.schema.raw('DROP TYPE IF EXISTS "color_name_enum" CASCADE');
this.schema.dropTable(this.tableName); this.schema.dropTable(this.tableName);
} }
} }

View File

@ -21,7 +21,7 @@ export default class Courses extends BaseSchema {
.enu('status', Object.values(CourseStatusEnum), { .enu('status', Object.values(CourseStatusEnum), {
useNative: true, useNative: true,
enumName: 'course_status_enum', enumName: 'course_status_enum',
existingType: true, existingType: false,
schemaName: 'public', schemaName: 'public',
}) })
.notNullable(); .notNullable();
@ -31,7 +31,7 @@ export default class Courses extends BaseSchema {
} }
public async down() { public async down() {
await this.schema.raw('DROP TYPE IF EXISTS "course_status" CASCADE'); await this.schema.raw('DROP TYPE IF EXISTS "course_status_enum" CASCADE');
this.schema.dropTable(this.tableName); this.schema.dropTable(this.tableName);
} }
} }

View File

@ -8,6 +8,7 @@ export default class Lessons extends BaseSchema {
table.string('id', 21).primary(); table.string('id', 21).primary();
table.string('course_id', 21).unsigned().references('courses.id').onDelete('CASCADE'); table.string('course_id', 21).unsigned().references('courses.id').onDelete('CASCADE');
table.string('title').notNullable(); table.string('title').notNullable();
table.integer('display_order').notNullable();
table.integer('color_id').unsigned().references('colors.id'); table.integer('color_id').unsigned().references('colors.id');
table.string('description'); table.string('description');
table.time('duration').notNullable(); table.time('duration').notNullable();

View File

@ -7,7 +7,7 @@ export default class CourseSeeder extends BaseSeeder {
public async run() { public async run() {
this.CourseFactory = CourseFactory; this.CourseFactory = CourseFactory;
await this.CourseFactory.with('lessons', 2, lessonFactory => lessonFactory.with('content')) await this.CourseFactory.with('lessons', 10, lessonFactory => lessonFactory.with('content'))
.with('category') .with('category')
.with('teacher') .with('teacher')
.createMany(3); .createMany(3);

View File

@ -3,6 +3,9 @@ import Route from '@ioc:Adonis/Core/Route';
Route.group(() => { Route.group(() => {
Route.get('/', 'Api/v1/LessonsController.list').middleware('role:admin,teacher,student').as('lessons.list'); Route.get('/', 'Api/v1/LessonsController.list').middleware('role:admin,teacher,student').as('lessons.list');
Route.post('/', 'Api/v1/LessonsController.create').middleware('role:admin,teacher').as('lessons.create'); Route.post('/', 'Api/v1/LessonsController.create').middleware('role:admin,teacher').as('lessons.create');
Route.post('/save-order', 'Api/v1/LessonsController.saveOrder')
.middleware('role:admin,teacher')
.as('lessons.update-order');
Route.get('/:id', 'Api/v1/LessonsController.show').middleware('role:admin,teacher,student').as('lessons.show'); Route.get('/:id', 'Api/v1/LessonsController.show').middleware('role:admin,teacher,student').as('lessons.show');
Route.delete('/:id', 'Api/v1/LessonsController.delete').middleware('role:admin,teacher').as('lessons.delete'); Route.delete('/:id', 'Api/v1/LessonsController.delete').middleware('role:admin,teacher').as('lessons.delete');
Route.put('/:id', 'Api/v1/LessonsController.update').middleware('role:admin,teacher').as('lessons.update'); Route.put('/:id', 'Api/v1/LessonsController.update').middleware('role:admin,teacher').as('lessons.update');