Skip to content

Commit

Permalink
EW-1001 Adding endpoint for tasks of a lesson (#5255)
Browse files Browse the repository at this point in the history
* adding route, dto, mapper and uc implementation
  • Loading branch information
psachmann authored Sep 27, 2024
1 parent 75da2f6 commit 1a61559
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 7 deletions.
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;
}
}

0 comments on commit 1a61559

Please sign in to comment.