Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EW-1001 Adding endpoint for tasks of a lesson #5255

Merged
merged 10 commits into from
Sep 27, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { EntityManager } from '@mikro-orm/core';
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { courseFactory, lessonFactory, taskFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing';
import { ServerTestModule } from '@src/modules/server';

describe('Lesson Controller (API) - GET list of lesson tasks /lessons/:lessonId/tasks', () => {
let module: TestingModule;
let app: INestApplication;
let em: EntityManager;
let testApiClient: TestApiClient;

beforeAll(async () => {
module = await Test.createTestingModule({
imports: [ServerTestModule],
}).compile();
app = module.createNestApplication();
em = module.get(EntityManager);
testApiClient = new TestApiClient(app, '/lessons');

await app.init();
});

afterAll(async () => {
await app.close();
await module.close();
});

describe('when lesson exists', () => {
const setup = async () => {
const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher();
const course = courseFactory.buildWithId({ teachers: [teacherUser] });
const lesson = lessonFactory.build({ course });
const tasks = taskFactory.buildList(3, { creator: teacherUser, lesson });

await em.persistAndFlush([teacherAccount, teacherUser, course, lesson, ...tasks]);

const loggedInClient = await testApiClient.login(teacherAccount);

return { loggedInClient, course, lesson, tasks };
};

it('should return a list of tasks', async () => {
const { loggedInClient, lesson } = await setup();

const response = await loggedInClient.get(`/${lesson.id}/tasks`);

expect(response.status).toBe(200);
expect((response.body as []).length).toEqual(3);
});
});
});
5 changes: 3 additions & 2 deletions apps/server/src/modules/lesson/controller/dto/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './lesson.url.params';
export { LessonsUrlParams } from './lessons.url.params';
export * from './lesson-content.response';
export * from './lesson-linked-task.response';
export * from './lesson.response';
export * from './lesson.url.params';
export { LessonsUrlParams } from './lessons.url.params';
export * from './material.response';
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ApiProperty } from '@nestjs/swagger';
import { InputFormat } from '@shared/domain/types';

export class LessonLinkedTaskResponse {
@ApiProperty()
public readonly name: string;

@ApiProperty()
public readonly description: string;

@ApiProperty({ enum: InputFormat })
public readonly descriptionInputFormat: InputFormat;

@ApiProperty({ type: Date, nullable: true })
availableDate?: Date;

@ApiProperty({ type: Date, nullable: true })
dueDate?: Date;

@ApiProperty()
public readonly private: boolean = true;

@ApiProperty({ nullable: true })
public readonly publicSubmissions?: boolean;

@ApiProperty({ nullable: true })
public readonly teamSubmissions?: boolean;

@ApiProperty({ nullable: true })
public readonly creator?: string;

@ApiProperty({ nullable: true })
public readonly courseId?: string;

@ApiProperty({ type: [String] })
public readonly submissionIds: string[] = [];

@ApiProperty({ type: [String] })
public readonly finishedIds: string[] = [];

constructor(props: Readonly<LessonLinkedTaskResponse>) {
this.name = props.name;
this.description = props.description;
this.descriptionInputFormat = props.descriptionInputFormat;
this.availableDate = props.availableDate;
this.dueDate = props.dueDate;
this.private = props.private;
this.creator = props.creator;
this.courseId = props.courseId;
this.publicSubmissions = props.publicSubmissions;
this.teamSubmissions = props.teamSubmissions;
this.submissionIds = props.submissionIds;
this.finishedIds = props.finishedIds;
}
}
11 changes: 11 additions & 0 deletions apps/server/src/modules/lesson/controller/lesson.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Controller, Delete, Get, Param } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { LessonUC } from '../uc';
import { LessonMetadataListResponse, LessonResponse, LessonUrlParams, LessonsUrlParams } from './dto';
import { LessonLinkedTaskResponse } from './dto/lesson-linked-task.response';
import { LessonMapper } from './mapper/lesson.mapper';

@ApiTags('Lesson')
Expand Down Expand Up @@ -33,4 +34,14 @@ export class LessonController {
const response = new LessonResponse(lesson);
return response;
}

@Get(':lessonId/tasks')
async getLessonTasks(
@Param() urlParams: LessonUrlParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<LessonLinkedTaskResponse[]> {
const tasks = await this.lessonUC.getLessonLinkedTasks(currentUser.userId, urlParams.lessonId);

return tasks;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { MikroORM } from '@mikro-orm/core';
import { setupEntities, submissionFactory, taskFactory, userFactory } from '@shared/testing';
import { LessonLinkedTaskResponse } from '../dto/lesson-linked-task.response';
import { LessonMapper } from './lesson.mapper';

describe('LessonMapper', () => {
let orm: MikroORM;

beforeAll(async () => {
orm = await setupEntities();
});

afterAll(async () => {
await orm.close();
});

describe('mapTaskToResponse', () => {
describe('when mapping task to response', () => {
const setup = () => {
const task = taskFactory.buildWithId({
publicSubmissions: true,
teamSubmissions: true,
submissions: submissionFactory.buildListWithId(2),
finished: userFactory.buildListWithId(2),
});

return { task };
};

it('should map task to response', () => {
const { task } = setup();

const result = LessonMapper.mapTaskToResponse(task);

expect(result).toEqual<LessonLinkedTaskResponse>({
name: task.name,
description: task.description,
descriptionInputFormat: task.descriptionInputFormat,
availableDate: task.availableDate,
dueDate: task.dueDate,
private: task.private,
creator: task.creator?.id,
publicSubmissions: task.publicSubmissions,
teamSubmissions: task.teamSubmissions,
courseId: task.course?.id,
submissionIds: task.submissions.toArray().map((submission) => submission.id),
finishedIds: task.finished.toArray().map((submission) => submission.id),
});
});
});
});
});
24 changes: 22 additions & 2 deletions apps/server/src/modules/lesson/controller/mapper/lesson.mapper.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
import { LessonEntity } from '@shared/domain/entity';
import { LessonEntity, Task } from '@shared/domain/entity';
import { LessonMetadataResponse } from '../dto';
import { LessonLinkedTaskResponse } from '../dto/lesson-linked-task.response';

export class LessonMapper {
static mapToMetadataResponse(lesson: LessonEntity): LessonMetadataResponse {
public static mapToMetadataResponse(lesson: LessonEntity): LessonMetadataResponse {
const dto = new LessonMetadataResponse({ _id: lesson.id, name: lesson.name });
return dto;
}

public static mapTaskToResponse(task: Task): LessonLinkedTaskResponse {
const response = new LessonLinkedTaskResponse({
name: task.name,
description: task.description,
descriptionInputFormat: task.descriptionInputFormat,
availableDate: task.availableDate,
dueDate: task.dueDate,
private: task.private,
creator: task.creator?.id,
publicSubmissions: task.publicSubmissions,
teamSubmissions: task.teamSubmissions,
courseId: task.course?.id,
submissionIds: task.submissions.toArray().map((submission) => submission.id),
finishedIds: task.finished.toArray().map((submission) => submission.id),
});

return response;
}
}
54 changes: 53 additions & 1 deletion apps/server/src/modules/lesson/uc/lesson.uc.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization';
import { CourseService } from '@modules/learnroom/service';
import { Test, TestingModule } from '@nestjs/testing';
import { Permission } from '@shared/domain/interface';
import { courseFactory, lessonFactory, setupEntities, userFactory } from '@shared/testing';
import { CourseService } from '@modules/learnroom/service';
import { LessonService } from '../service';
import { LessonUC } from './lesson.uc';

Expand Down Expand Up @@ -193,4 +193,56 @@ describe('LessonUC', () => {
});
});
});

describe('getLessonLinkedTasks', () => {
describe('when user is a valid teacher', () => {
const setup = () => {
const user = userFactory.buildWithId();
const lesson = lessonFactory.buildWithId();

authorizationService.getUserWithPermissions.mockResolvedValueOnce(user);
lessonService.findById.mockResolvedValueOnce(lesson);

authorizationService.hasPermission.mockReturnValueOnce(true);

return { user, lesson };
};

it('should get user with permissions from authorizationService', async () => {
const { user } = setup();

await lessonUC.getLessonLinkedTasks(user.id, 'lessonId');

expect(authorizationService.getUserWithPermissions).toHaveBeenCalledWith(user.id);
});

it('should get lesson from lessonService', async () => {
const { user, lesson } = setup();

await lessonUC.getLessonLinkedTasks(user.id, lesson.id);

expect(lessonService.findById).toHaveBeenCalledWith(lesson.id);
});

it('should return check permission', async () => {
const { user, lesson } = setup();

await lessonUC.getLessonLinkedTasks(user.id, lesson.id);

expect(authorizationService.checkPermission).toHaveBeenCalledWith(
expect.objectContaining({ ...user }),
expect.objectContaining({ ...lesson }),
AuthorizationContextBuilder.read([Permission.TOPIC_VIEW])
);
});

it('should return tasks', async () => {
const { user, lesson } = setup();

const result = await lessonUC.getLessonLinkedTasks(user.id, lesson.id);

expect(result).toEqual(lesson.getLessonLinkedTasks().map((task) => task));
});
});
});
});
17 changes: 15 additions & 2 deletions apps/server/src/modules/lesson/uc/lesson.uc.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization';
import { CourseService } from '@modules/learnroom/service/course.service';
import { Injectable } from '@nestjs/common';
import { LessonEntity } from '@shared/domain/entity';
import { Permission } from '@shared/domain/interface';
import { EntityId } from '@shared/domain/types';
import { LessonEntity } from '@shared/domain/entity';
import { CourseService } from '@modules/learnroom/service/course.service';
import { LessonLinkedTaskResponse } from '../controller/dto/lesson-linked-task.response';
import { LessonMapper } from '../controller/mapper/lesson.mapper';
import { LessonService } from '../service';

@Injectable()
Expand Down Expand Up @@ -51,4 +53,15 @@ export class LessonUC {

return lesson;
}

async getLessonLinkedTasks(userId: EntityId, lessonId: EntityId): Promise<LessonLinkedTaskResponse[]> {
const user = await this.authorizationService.getUserWithPermissions(userId);
const lesson = await this.lessonService.findById(lessonId);

this.authorizationService.checkPermission(user, lesson, AuthorizationContextBuilder.read([Permission.TOPIC_VIEW]));

const tasks = lesson.getLessonLinkedTasks().map((task) => LessonMapper.mapTaskToResponse(task));

return tasks;
}
}
Loading