From c3a8fbe3c37be16d1607eaa649f3a5e294102015 Mon Sep 17 00:00:00 2001 From: Caaf14 <cata-araya@outlook.com> Date: Sun, 21 Jan 2024 22:11:16 -0500 Subject: [PATCH 1/7] qcm and qrl model done --- server/app/model/database/qcm.ts | 34 +++ server/app/model/database/qrl.ts | 25 ++ server/app/model/dto/qcm/create-qcm.dto.ts | 30 ++ server/app/model/dto/qcm/qcm.dto.constants.ts | 4 + server/app/model/dto/qcm/update-qcm.dto.ts | 34 +++ server/app/model/dto/qrl/create-qrl.dto.ts | 20 ++ server/app/model/dto/qrl/qrl.dto.constants.ts | 2 + server/app/model/dto/qrl/update-qrl.dto.ts | 26 ++ server/app/services/qcm/qcm.service.spec.ts | 285 ++++++++++++++++++ server/app/services/qcm/qcm.service.ts | 156 ++++++++++ server/app/services/qrl/qrl.service.spec.ts | 246 +++++++++++++++ server/app/services/qrl/qrl.service.ts | 126 ++++++++ 12 files changed, 988 insertions(+) create mode 100644 server/app/model/database/qcm.ts create mode 100644 server/app/model/database/qrl.ts create mode 100644 server/app/model/dto/qcm/create-qcm.dto.ts create mode 100644 server/app/model/dto/qcm/qcm.dto.constants.ts create mode 100644 server/app/model/dto/qcm/update-qcm.dto.ts create mode 100644 server/app/model/dto/qrl/create-qrl.dto.ts create mode 100644 server/app/model/dto/qrl/qrl.dto.constants.ts create mode 100644 server/app/model/dto/qrl/update-qrl.dto.ts create mode 100644 server/app/services/qcm/qcm.service.spec.ts create mode 100644 server/app/services/qcm/qcm.service.ts create mode 100644 server/app/services/qrl/qrl.service.spec.ts create mode 100644 server/app/services/qrl/qrl.service.ts diff --git a/server/app/model/database/qcm.ts b/server/app/model/database/qcm.ts new file mode 100644 index 00000000..9af8c336 --- /dev/null +++ b/server/app/model/database/qcm.ts @@ -0,0 +1,34 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { ApiProperty } from '@nestjs/swagger'; +import { Document } from 'mongoose'; + +export type QcmDocument = Qcm & Document; + +export class Choice { + choice: string; + correct: boolean; + } + +@Schema() +export class Qcm { + @ApiProperty() + @Prop({ required: true }) + question: string; + + @ApiProperty() + @Prop({ required: true }) + points: number; + + @ApiProperty() + @Prop({ required: true }) + choices: Choice[]; + + @ApiProperty() + @Prop({ required: true }) + lastModified: string; + + @ApiProperty() + _id?: string; +} + +export const qcmSchema = SchemaFactory.createForClass(Qcm); diff --git a/server/app/model/database/qrl.ts b/server/app/model/database/qrl.ts new file mode 100644 index 00000000..da2d7c2e --- /dev/null +++ b/server/app/model/database/qrl.ts @@ -0,0 +1,25 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { ApiProperty } from '@nestjs/swagger'; +import { Document } from 'mongoose'; + +export type QrlDocument = Qrl & Document; + +@Schema() +export class Qrl { + @ApiProperty() + @Prop({ required: true }) + question: string; + + @ApiProperty() + @Prop({ required: true }) + points: number; + + @ApiProperty() + @Prop({ required: true }) + lastModified: string; + + @ApiProperty() + _id?: string; +} + +export const qrlSchema = SchemaFactory.createForClass(Qrl); diff --git a/server/app/model/dto/qcm/create-qcm.dto.ts b/server/app/model/dto/qcm/create-qcm.dto.ts new file mode 100644 index 00000000..d39f4ae8 --- /dev/null +++ b/server/app/model/dto/qcm/create-qcm.dto.ts @@ -0,0 +1,30 @@ +import { Choice } from '@app/model/database/qcm'; +import { QCM_MAX_CHOICES, QCM_MAX_POINTS, QCM_MIN_CHOICES, QCM_MIN_POINTS } from '@app/model/dto/qcm/qcm.dto.constants'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { ArrayMaxSize, ArrayMinSize, IsArray, IsInt, IsString, Max, Min, ValidateNested } from 'class-validator'; + +export class CreateQcmDto { + @ApiProperty() + @IsString() + question: string; + + @ApiProperty() + @IsInt() + @Min(QCM_MIN_POINTS) + @Max(QCM_MAX_POINTS) + points: number; + + @ApiProperty({ type: () => Choice, isArray: true }) + @IsArray() + @ArrayMinSize(QCM_MIN_CHOICES) + @ArrayMaxSize(QCM_MAX_CHOICES) + @ValidateNested({ each: true }) + @Type(() => Choice) + choices: Choice[]; + + @ApiProperty() + @IsString() + lastModified?: string; + +} diff --git a/server/app/model/dto/qcm/qcm.dto.constants.ts b/server/app/model/dto/qcm/qcm.dto.constants.ts new file mode 100644 index 00000000..b0968f76 --- /dev/null +++ b/server/app/model/dto/qcm/qcm.dto.constants.ts @@ -0,0 +1,4 @@ +export const QCM_MAX_POINTS = 100; +export const QCM_MIN_POINTS = 10; +export const QCM_MAX_CHOICES = 4; +export const QCM_MIN_CHOICES = 2; \ No newline at end of file diff --git a/server/app/model/dto/qcm/update-qcm.dto.ts b/server/app/model/dto/qcm/update-qcm.dto.ts new file mode 100644 index 00000000..d494e1b0 --- /dev/null +++ b/server/app/model/dto/qcm/update-qcm.dto.ts @@ -0,0 +1,34 @@ +import { Choice } from '@app/model/database/qcm'; +import { QCM_MAX_CHOICES, QCM_MAX_POINTS, QCM_MIN_CHOICES, QCM_MIN_POINTS } from '@app/model/dto/qcm/qcm.dto.constants'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { ArrayMaxSize, ArrayMinSize, IsArray, IsInt, IsNotEmpty, IsOptional, IsString, Max, Min, ValidateNested } from 'class-validator'; + +export class UpdateQcmDto { + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + question?: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsInt() + @Min(QCM_MIN_POINTS) + @Max(QCM_MAX_POINTS) + points?: number; + + @ApiProperty({ type: () => Choice, isArray: true, required: false }) + @IsOptional() + @IsArray() + @ArrayMinSize(QCM_MIN_CHOICES) + @ArrayMaxSize(QCM_MAX_CHOICES) + @ValidateNested({ each: true }) + @Type(() => Choice) + choices?: Choice[]; + + @ApiProperty() + @IsNotEmpty() + @IsString() + _id?: string; + +} diff --git a/server/app/model/dto/qrl/create-qrl.dto.ts b/server/app/model/dto/qrl/create-qrl.dto.ts new file mode 100644 index 00000000..87cc4d40 --- /dev/null +++ b/server/app/model/dto/qrl/create-qrl.dto.ts @@ -0,0 +1,20 @@ +import { QRL_MAX_POINTS, QRL_MIN_POINTS } from '@app/model/dto/qrl/qrl.dto.constants'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt, IsString, Max, Min } from 'class-validator'; + +export class CreateQrlDto { + @ApiProperty() + @IsString() + question: string; + + @ApiProperty() + @IsInt() + @Min(QRL_MIN_POINTS) + @Max(QRL_MAX_POINTS) + points: number; + + @ApiProperty() + @IsString() + lastModified: string; + +} diff --git a/server/app/model/dto/qrl/qrl.dto.constants.ts b/server/app/model/dto/qrl/qrl.dto.constants.ts new file mode 100644 index 00000000..c7c1155a --- /dev/null +++ b/server/app/model/dto/qrl/qrl.dto.constants.ts @@ -0,0 +1,2 @@ +export const QRL_MAX_POINTS = 100; +export const QRL_MIN_POINTS = 10; \ No newline at end of file diff --git a/server/app/model/dto/qrl/update-qrl.dto.ts b/server/app/model/dto/qrl/update-qrl.dto.ts new file mode 100644 index 00000000..a0ba07e3 --- /dev/null +++ b/server/app/model/dto/qrl/update-qrl.dto.ts @@ -0,0 +1,26 @@ +import { QRL_MAX_POINTS, QRL_MIN_POINTS } from '@app/model/dto/qrl/qrl.dto.constants'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt, IsNotEmpty, IsOptional, IsString, Max, Min } from 'class-validator'; + +export class UpdateQrlDto { + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + question?: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsInt() + @Min(QRL_MIN_POINTS) + @Max(QRL_MAX_POINTS) + points?: number; + + @ApiProperty() + @IsString() + lastModified: string; + + @ApiProperty() + @IsNotEmpty() + @IsString() + _id?: string; +} diff --git a/server/app/services/qcm/qcm.service.spec.ts b/server/app/services/qcm/qcm.service.spec.ts new file mode 100644 index 00000000..b254c933 --- /dev/null +++ b/server/app/services/qcm/qcm.service.spec.ts @@ -0,0 +1,285 @@ +import { DateService } from '@app/services/date/date.service'; +import { Logger } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { Connection, Model } from 'mongoose'; +import { QcmService } from './qcm.service'; + +import { Qcm, QcmDocument, qcmSchema } from '@app/model/database/qcm'; +import { MongooseModule, getConnectionToken, getModelToken } from '@nestjs/mongoose'; + +/** + * There is two way to test the service : + * - Mock the mongoose Model implementation and do what ever we want to do with it (see describe QcmService) or + * - Use mongodb memory server implementation (see describe QcmServiceEndToEnd) and let everything go through as if we had a real database + * + * The second method is generally better because it tests the database queries too. + * We will use it more + */ + +describe('QcmService', () => { + let service: QcmService; + let qcmModel: Model<QcmDocument>; + + beforeEach(async () => { + // notice that only the functions we call from the model are mocked + // we can´t use sinon because mongoose Model is an interface + qcmModel = { + countDocuments: jest.fn(), + insertMany: jest.fn(), + create: jest.fn(), + find: jest.fn(), + findOne: jest.fn(), + deleteOne: jest.fn(), + update: jest.fn(), + updateOne: jest.fn(), + } as unknown as Model<QcmDocument>; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QcmService, + Logger, + DateService, + { + provide: getModelToken(Qcm.name), + useValue: qcmModel, + }, + ], + }).compile(); + + service = module.get<QcmService>(QcmService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('database should be populated when there is no data', async () => { + jest.spyOn(qcmModel, 'countDocuments').mockResolvedValue(0); + const spyPopulateDB = jest.spyOn(service, 'populateDB'); + await service.start(); + expect(spyPopulateDB).toHaveBeenCalled(); + }); + + it('database should not be populated when there is some data', async () => { + jest.spyOn(qcmModel, 'countDocuments').mockResolvedValue(1); + const spyPopulateDB = jest.spyOn(service, 'populateDB'); + await service.start(); + expect(spyPopulateDB).not.toHaveBeenCalled(); + }); +}); + +const DELAY_BEFORE_CLOSING_CONNECTION = 200; + +describe('QcmServiceEndToEnd', () => { + let service: QcmService; + let qcmModel: Model<QcmDocument>; + let mongoServer: MongoMemoryServer; + let connection: Connection; + + beforeEach(async () => { + mongoServer = await MongoMemoryServer.create(); + // notice that only the functions we call from the model are mocked + // we can´t use sinon because mongoose Model is an interface + const module = await Test.createTestingModule({ + imports: [ + MongooseModule.forRootAsync({ + useFactory: () => ({ + uri: mongoServer.getUri(), + }), + }), + MongooseModule.forFeature([{ name: Qcm.name, schema: qcmSchema }]), + ], + providers: [QcmService, Logger, DateService], + }).compile(); + + service = module.get<QcmService>(QcmService); + qcmModel = module.get<Model<QcmDocument>>(getModelToken(Qcm.name)); + connection = await module.get(getConnectionToken()); + }); + + afterEach((done) => { + // The database get auto populated in the constructor + // We want to make sur we close the connection after the database got + // populated. So we add small delay + setTimeout(async () => { + await connection.close(); + await mongoServer.stop(); + done(); + }, DELAY_BEFORE_CLOSING_CONNECTION); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + expect(qcmModel).toBeDefined(); + }); + + it('start() should populate the database when there is no data', async () => { + const spyPopulateDB = jest.spyOn(service, 'populateDB'); + await qcmModel.deleteMany({}); + await service.start(); + expect(spyPopulateDB).toHaveBeenCalled(); + }); + + it('start() should not populate the DB when there is some data', async () => { + const qcm = getFakeQcm(); + await qcmModel.create(qcm); + const spyPopulateDB = jest.spyOn(service, 'populateDB'); + expect(spyPopulateDB).not.toHaveBeenCalled(); + }); + + it('populateDB() should add 2 new qcms', async () => { + const eltCountsBefore = await qcmModel.countDocuments(); + await service.populateDB(); + const eltCountsAfter = await qcmModel.countDocuments(); + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect(eltCountsAfter - eltCountsBefore).toEqual(2); + }); + + it('getAllQcms() return all qcms in database', async () => { + await qcmModel.deleteMany({}); + expect((await service.getAllQcm()).length).toEqual(0); + const qcm = getFakeQcm(); + await qcmModel.create(qcm); + expect((await service.getAllQcm()).length).toEqual(1); + }); + + it('getQcmByQuestion() return qcm with the specified question', async () => { + const qcm = getFakeQcm(); + await qcmModel.create(qcm); + expect(await service.getQcmByQuestion(qcm.question)).toEqual(expect.objectContaining(qcm)); + }); + + it('getQcmQuestion() should return qcm question', async () => { + const qcm = getFakeQcm(); + await qcmModel.create(qcm); + const question = await service.getQcmQuestion(qcm.question); + expect(question).toEqual(qcm.question); + }); + + it('getQcmQuestion() should fail if qcm does not exist', async () => { + const qcm = getFakeQcm(); + await expect(service.getQcmQuestion(qcm.question)).rejects.toBeTruthy(); + }); + + it('getQcmChoices() should return qcm question', async () => { + const qcm = getFakeQcm(); + await qcmModel.create(qcm); + const choices = await service.getQcmChoices(qcm.question); + expect(choices).toEqual(qcm.choices); + }); + + it('getQcmChoices() should fail if qcm does not exist', async () => { + const qcm = getFakeQcm(); + await expect(service.getQcmChoices(qcm.question)).rejects.toBeTruthy(); + }); + + it('modifyQcm() should fail if a qcm with same question exists', async () => { + const qcm = getFakeQcm(); + const qcm2 = getFakeQcm2(); + await qcmModel.create(qcm); + await qcmModel.create(qcm2); + qcm.question = qcm2.question; + await expect(service.modifyQcm(qcm)).rejects.toBeTruthy(); + }); + + it('modifyQcm() should fail if qcm does not exist', async () => { + const qcm = getFakeQcm(); + await expect(service.modifyQcm(qcm)).rejects.toBeTruthy(); + }); + + it('modifyQcm() should fail if mongo query failed', async () => { + jest.spyOn(qcmModel, 'updateOne').mockRejectedValue(''); + const qcm = getFakeQcm(); + await expect(service.modifyQcm(qcm)).rejects.toBeTruthy(); + }); + + it('deleteQcm() should delete the qcm', async () => { + await qcmModel.deleteMany({}); + const qcm = getFakeQcm(); + await qcmModel.create(qcm); + await service.deleteQcm(qcm.question); + expect(await qcmModel.countDocuments()).toEqual(0); + }); + + it('deleteQcm() should fail if the qcm does not exist', async () => { + await qcmModel.deleteMany({}); + const qcm = getFakeQcm(); + await expect(service.deleteQcm(qcm.question)).rejects.toBeTruthy(); + }); + + it('deleteQcm() should fail if mongo query failed', async () => { + jest.spyOn(qcmModel, 'deleteOne').mockRejectedValue(''); + const qcm = getFakeQcm(); + await expect(service.deleteQcm(qcm.question)).rejects.toBeTruthy(); + }); + + it('addQcm() should add the qcm to the DB', async () => { + await qcmModel.deleteMany({}); + const qcm = getFakeQcm(); + await service.addQcm({ ...qcm, question: 'Question1', points: 50 }); + expect(await qcmModel.countDocuments()).toEqual(1); + }); + + it('addQcm() should fail if question already is in DB', async () => { + await qcmModel.deleteMany({}); + const qcm = getFakeQcm(); + + await qcmModel.create(qcm); + await expect(service.addQcm(qcm)).rejects.toBeTruthy(); + expect(await qcmModel.countDocuments()).toEqual(1); + }); + + it('addQcm() should fail if mongo query failed', async () => { + jest.spyOn(qcmModel, 'create').mockImplementation(async () => Promise.reject('')); + const qcm = getFakeQcm(); + await expect(service.addQcm({ ...qcm, question: 'Question1', points: 50 })).rejects.toBeTruthy(); + }); + + it('addQcm() should fail if the qcm is not a valid', async () => { + const qcm = getFakeQcm(); + await expect(service.addQcm({ ...qcm, question: 'Question1', points: 35,choices: [ + { choice: 'Oracles', correct: true }, + { choice: 'Oracle', correct: false }, + ] })).rejects.toBeTruthy(); + await expect(service.addQcm({ ...qcm, question: 'Question1', points: 0,choices: [ + { choice: 'Oracles', correct: true }, + { choice: 'Oracle', correct: false }, + ] })).rejects.toBeTruthy(); + await expect(service.addQcm({ ...qcm, question: 'Question1', points: 110,choices: [ + { choice: 'Oracles', correct: true }, + { choice: 'Oracle', correct: false }, + ] })).rejects.toBeTruthy(); + await expect(service.addQcm({ ...qcm, question: 'Question1', points: 50,choices: [ + { choice: 'Oracles', correct: true }, + { choice: 'Oracle', correct: true }, + ] })).rejects.toBeTruthy(); + await expect(service.addQcm({ ...qcm, question: 'Question1', points: 50,choices: [ + { choice: 'Oracles', correct: true }, + { choice: 'Oracle', correct: false }, + { choice: 'Oracle', correct: false }, + ] })).rejects.toBeTruthy(); + }); +}); + +const getFakeQcm = (): Qcm => ({ + question: 'Quel est le nom de ce jeu?', + points: 100, + choices: [ + { choice: 'Oracles', correct: true }, + { choice: 'Oracle', correct: false }, + ], + lastModified: getRandomString(), +}); +const getFakeQcm2 = (): Qcm => ({ + question: 'Quel est le nom de ce jeu2?', + points: 100, + choices: [ + { choice: 'Oracles', correct: true }, + { choice: 'Oracle', correct: false }, + ], + lastModified: getRandomString(), +}); + +const BASE_36 = 36; +const getRandomString = (): string => (Math.random() + 1).toString(BASE_36).substring(2); diff --git a/server/app/services/qcm/qcm.service.ts b/server/app/services/qcm/qcm.service.ts new file mode 100644 index 00000000..a4dbf8bf --- /dev/null +++ b/server/app/services/qcm/qcm.service.ts @@ -0,0 +1,156 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; + +import { Choice, Qcm, QcmDocument } from '@app/model/database/qcm'; +import { CreateQcmDto } from '@app/model/dto/qcm/create-qcm.dto'; +import { UpdateQcmDto } from '@app/model/dto/qcm/update-qcm.dto'; +import { DateService } from '@app/services/date/date.service'; + +const MAXIMUM_NUMBER_OF_CREDITS = 6; + +@Injectable() +export class QcmService { + constructor( + @InjectModel(Qcm.name) public qcmModel: Model<QcmDocument>, + private readonly logger: Logger, + private readonly dateService: DateService, + ) { + this.start(); + } + + async start() { + if ((await this.qcmModel.countDocuments()) === 0) { + await this.populateDB(); + } + } + + async populateDB(): Promise<void> { + const qcmQuestions: CreateQcmDto[] = [ + { + question: 'Dans quelle ville est poly?', + points: 10, + choices: [ + { choice: 'Montreal', correct: true }, + { choice: 'Quebec', correct: false }, + { choice: 'Sherbrooke', correct: false }, + { choice: 'Toronto', correct: false }, + ], + lastModified: this.dateService.currentTime(), + }, + { + question: 'Combien de membres dans cette équipe de projet 2?', + points: 100, + choices: [ + { choice: '6', correct: true }, + { choice: '5', correct: false }, + ], + lastModified: this.dateService.currentTime(), + }, + ]; + + this.logger.log('THIS ADDS DATA TO THE DATABASE, DO NOT USE OTHERWISE'); + await this.qcmModel.insertMany(qcmQuestions); + } + + async getAllQcm(): Promise<Qcm[]> { + return await this.qcmModel.find({}); + } + + async getQcmByQuestion(question: string): Promise<Qcm> { + // NB: This can return null if the qcm does not exist, you need to handle it + return await this.qcmModel.findOne({ question: question }); + } + + async addQcm(qcm: CreateQcmDto): Promise<void> { + + if (!this.validateQcm(qcm)) { + return Promise.reject('Invalid qcm'); + } + const existingQcm = await this.getQcmByQuestion(qcm.question); + if(existingQcm){ + return Promise.reject('Qcm already exists'); + } + const currentDate = this.dateService.currentTime(); + const newQcm = { ...qcm, lastModified: currentDate }; + try { + await this.qcmModel.create(newQcm); + } catch (error) { + return Promise.reject(`Failed to insert course: ${error}`); + } + } + + async deleteQcm(question: string): Promise<void> { + try { + const res = await this.qcmModel.deleteOne({ question: question }); + if (res.deletedCount === 0) { + return Promise.reject('Could not find course'); + } + } catch (error) { + return Promise.reject(`Failed to delete course: ${error}`); + } + } + + async modifyQcm(qcm: UpdateQcmDto): Promise<void> { + const currentDate = this.dateService.currentTime(); + const filterQuery = { id: qcm._id }; + const updatedQcm = { ...qcm, lastModified: currentDate }; + // Can also use replaceOne if we want to replace the entire object + try { + const qcmFound = await this.getQcmByQuestion(qcm.question); + if(!qcmFound){ + return Promise.reject('Qcm does not exist'); + } + if(qcmFound && qcm._id !== qcmFound._id ){ + return Promise.reject('Another qcm already has the same question'); + } + const res = await this.qcmModel.replaceOne(filterQuery, updatedQcm); + if (res.matchedCount === 0) { + return Promise.reject('Could not find qcm'); + } + } catch (error) { + return Promise.reject(`Failed to update qcm: ${error}`); + } + } + + async getQcmQuestion(question: string): Promise<string> { + const filterQuery = { question: question }; + // Only get the choices and not any of the other fields + try { + const res = await this.qcmModel.findOne(filterQuery, {question: 1}); + return res.question; + } catch (error) { + return Promise.reject(`Failed to get data: ${error}`); + } + } + async getQcmChoices(question: string): Promise<Array<Choice>> { + const filterQuery = { question: question }; + // Only get the choices and not any of the other fields + try { + const res = await this.qcmModel.findOne(filterQuery, {choices: 1}); + return res.choices; + } catch (error) { + return Promise.reject(`Failed to get data: ${error}`); + } + } + + private validateQcm(qcm: CreateQcmDto): boolean { + return this.validatePoints(qcm.points) && this.validateChoices(qcm.choices); + } + private validatePoints(points: number): boolean { + return points % 10 === 0 && points >= 10 && points <= 100; + } + private validateChoices(choices: Choice[]): boolean { + let correctChoices = 0; + let incorrectChoices = 0; + choices.forEach(choice => { + if (choice.correct) { + correctChoices++; + } else { + incorrectChoices++; + } + }); + let sumChoices = correctChoices + incorrectChoices; + return (correctChoices >= 1 && incorrectChoices >= 1) && (sumChoices == 4 || sumChoices == 2); + } +} diff --git a/server/app/services/qrl/qrl.service.spec.ts b/server/app/services/qrl/qrl.service.spec.ts new file mode 100644 index 00000000..7432290b --- /dev/null +++ b/server/app/services/qrl/qrl.service.spec.ts @@ -0,0 +1,246 @@ +import { DateService } from '@app/services/date/date.service'; +import { Logger } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { Connection, Model } from 'mongoose'; +import { QrlService } from './qrl.service'; + +import { Qrl, QrlDocument, qrlSchema } from '@app/model/database/qrl'; +import { MongooseModule, getConnectionToken, getModelToken } from '@nestjs/mongoose'; + +/** + * There is two way to test the service : + * - Mock the mongoose Model implementation and do what ever we want to do with it (see describe QrlService) or + * - Use mongodb memory server implementation (see describe QrlServiceEndToEnd) and let everything go through as if we had a real database + * + * The second method is generally better because it tests the database queries too. + * We will use it more + */ + +describe('QrlService', () => { + let service: QrlService; + let qrlModel: Model<QrlDocument>; + + beforeEach(async () => { + // notice that only the functions we call from the model are mocked + // we can´t use sinon because mongoose Model is an interface + qrlModel = { + countDocuments: jest.fn(), + insertMany: jest.fn(), + create: jest.fn(), + find: jest.fn(), + findOne: jest.fn(), + deleteOne: jest.fn(), + update: jest.fn(), + updateOne: jest.fn(), + } as unknown as Model<QrlDocument>; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QrlService, + Logger, + DateService, + { + provide: getModelToken(Qrl.name), + useValue: qrlModel, + }, + ], + }).compile(); + + service = module.get<QrlService>(QrlService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('database should be populated when there is no data', async () => { + jest.spyOn(qrlModel, 'countDocuments').mockResolvedValue(0); + const spyPopulateDB = jest.spyOn(service, 'populateDB'); + await service.start(); + expect(spyPopulateDB).toHaveBeenCalled(); + }); + + it('database should not be populated when there is some data', async () => { + jest.spyOn(qrlModel, 'countDocuments').mockResolvedValue(1); + const spyPopulateDB = jest.spyOn(service, 'populateDB'); + await service.start(); + expect(spyPopulateDB).not.toHaveBeenCalled(); + }); +}); + +const DELAY_BEFORE_CLOSING_CONNECTION = 200; + +describe('QrlServiceEndToEnd', () => { + let service: QrlService; + let qrlModel: Model<QrlDocument>; + let mongoServer: MongoMemoryServer; + let connection: Connection; + + beforeEach(async () => { + mongoServer = await MongoMemoryServer.create(); + // notice that only the functions we call from the model are mocked + // we can´t use sinon because mongoose Model is an interface + const module = await Test.createTestingModule({ + imports: [ + MongooseModule.forRootAsync({ + useFactory: () => ({ + uri: mongoServer.getUri(), + }), + }), + MongooseModule.forFeature([{ name: Qrl.name, schema: qrlSchema }]), + ], + providers: [QrlService, Logger, DateService], + }).compile(); + + service = module.get<QrlService>(QrlService); + qrlModel = module.get<Model<QrlDocument>>(getModelToken(Qrl.name)); + connection = await module.get(getConnectionToken()); + }); + + afterEach((done) => { + // The database get auto populated in the constructor + // We want to make sur we close the connection after the database got + // populated. So we add small delay + setTimeout(async () => { + await connection.close(); + await mongoServer.stop(); + done(); + }, DELAY_BEFORE_CLOSING_CONNECTION); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + expect(qrlModel).toBeDefined(); + }); + + it('start() should populate the database when there is no data', async () => { + const spyPopulateDB = jest.spyOn(service, 'populateDB'); + await qrlModel.deleteMany({}); + await service.start(); + expect(spyPopulateDB).toHaveBeenCalled(); + }); + + it('start() should not populate the DB when there is some data', async () => { + const qrl = getFakeQrl(); + await qrlModel.create(qrl); + const spyPopulateDB = jest.spyOn(service, 'populateDB'); + expect(spyPopulateDB).not.toHaveBeenCalled(); + }); + + it('populateDB() should add 2 new qrls', async () => { + const eltCountsBefore = await qrlModel.countDocuments(); + await service.populateDB(); + const eltCountsAfter = await qrlModel.countDocuments(); + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect(eltCountsAfter - eltCountsBefore).toEqual(2); + }); + + it('getAllQrl() return all qrls in database', async () => { + await qrlModel.deleteMany({}); + expect((await service.getAllQrl()).length).toEqual(0); + const qrl = getFakeQrl(); + await qrlModel.create(qrl); + expect((await service.getAllQrl()).length).toEqual(1); + }); + + it('getQrlByQuestion() return qrl with the specified question', async () => { + const qrl = getFakeQrl(); + await qrlModel.create(qrl); + expect(await service.getQrlByQuestion(qrl.question)).toEqual(expect.objectContaining(qrl)); + }); + + it('getQrlQuestion() should return qrl question', async () => { + const qrl = getFakeQrl(); + await qrlModel.create(qrl); + const question = await service.getQrlQuestion(qrl.question); + expect(question).toEqual(qrl.question); + }); + + it('getQrlQuestion() should fail if qrl does not exist', async () => { + const qrl = getFakeQrl(); + await expect(service.getQrlQuestion(qrl.question)).rejects.toBeTruthy(); + }); + + it('modifyQrl() should fail if a qrl with same question exists', async () => { + const qrl = getFakeQrl(); + const qrl2 = getFakeQrl2(); + await qrlModel.create(qrl); + await qrlModel.create(qrl2); + qrl.question = qrl2.question; + await expect(service.modifyQrl(qrl)).rejects.toBeTruthy(); + }); + + it('modifyQrl() should fail if qrl does not exist', async () => { + const qrl = getFakeQrl(); + await expect(service.modifyQrl(qrl)).rejects.toBeTruthy(); + }); + + it('modifyQrl() should fail if mongo query failed', async () => { + jest.spyOn(qrlModel, 'updateOne').mockRejectedValue(''); + const qrl = getFakeQrl(); + await expect(service.modifyQrl(qrl)).rejects.toBeTruthy(); + }); + + it('deleteQrl() should delete the qrl', async () => { + await qrlModel.deleteMany({}); + const qrl = getFakeQrl(); + await qrlModel.create(qrl); + await service.deleteQrl(qrl.question); + expect(await qrlModel.countDocuments()).toEqual(0); + }); + + it('deleteQrl() should fail if the qrl does not exist', async () => { + await qrlModel.deleteMany({}); + const qrl = getFakeQrl(); + await expect(service.deleteQrl(qrl.question)).rejects.toBeTruthy(); + }); + + it('deleteQrl() should fail if mongo query failed', async () => { + jest.spyOn(qrlModel, 'deleteOne').mockRejectedValue(''); + const qrl = getFakeQrl(); + await expect(service.deleteQrl(qrl.question)).rejects.toBeTruthy(); + }); + + it('addQrl() should add the qrl to the DB', async () => { + await qrlModel.deleteMany({}); + const qrl = getFakeQrl(); + await service.addQrl(qrl); + expect(await qrlModel.countDocuments()).toEqual(1); + }); + + it('addQrl() should fail if question already is in DB', async () => { + await qrlModel.deleteMany({}); + const qrl = getFakeQrl(); + await qrlModel.create(qrl); + await expect(service.addQrl(qrl)).rejects.toBeTruthy(); + expect(await qrlModel.countDocuments()).toEqual(1); + }); + + it('addQrl() should fail if mongo query failed', async () => { + jest.spyOn(qrlModel, 'create').mockImplementation(async () => Promise.reject('')); + const qrl = getFakeQrl(); + await expect(service.addQrl({ ...qrl, question: 'Question1', points: 50 })).rejects.toBeTruthy(); + }); + + it('addQrl() should fail if the qrl is not a valid', async () => { + const qrl = getFakeQrl(); + await expect(service.addQrl({ ...qrl, question: 'Question1', points: 35})).rejects.toBeTruthy(); + await expect(service.addQrl({ ...qrl, question: 'Question1', points: 0})).rejects.toBeTruthy(); + await expect(service.addQrl({ ...qrl, question: 'Question1', points: 110})).rejects.toBeTruthy(); + }); +}); + +const getFakeQrl = (): Qrl => ({ + question: 'Quel est le nom de ce jeu?', + points: 100, + lastModified: getRandomString(), +}); +const getFakeQrl2 = (): Qrl => ({ + question: 'Quel est le nom de ce jeu2?', + points: 100, + lastModified: getRandomString(), +}); + +const BASE_36 = 36; +const getRandomString = (): string => (Math.random() + 1).toString(BASE_36).substring(2); diff --git a/server/app/services/qrl/qrl.service.ts b/server/app/services/qrl/qrl.service.ts new file mode 100644 index 00000000..dc9afaf6 --- /dev/null +++ b/server/app/services/qrl/qrl.service.ts @@ -0,0 +1,126 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; + +import { Qrl, QrlDocument } from '@app/model/database/qrl'; +import { CreateQrlDto } from '@app/model/dto/qrl/create-qrl.dto'; +import { UpdateQrlDto } from '@app/model/dto/qrl/update-qrl.dto'; +import { DateService } from '@app/services/date/date.service'; + +const MAXIMUM_NUMBER_OF_CREDITS = 6; + +@Injectable() +export class QrlService { + constructor( + @InjectModel(Qrl.name) public qrlModel: Model<QrlDocument>, + private readonly logger: Logger, + private readonly dateService: DateService, + ) { + this.start(); + } + + async start() { + if ((await this.qrlModel.countDocuments()) === 0) { + await this.populateDB(); + } + } + + async populateDB(): Promise<void> { + const qrlQuestions: CreateQrlDto[] = [ + { + question: 'Dans quelle ville est poly?', + points: 10, + lastModified: this.dateService.currentTime(), + }, + { + question: 'Combien de membres dans cette équipe de projet 2?', + points: 100, + lastModified: this.dateService.currentTime(), + }, + ]; + + this.logger.log('THIS ADDS DATA TO THE DATABASE, DO NOT USE OTHERWISE'); + await this.qrlModel.insertMany(qrlQuestions); + } + + async getAllQrl(): Promise<Qrl[]> { + return await this.qrlModel.find({}); + } + + async getQrlById(id: string): Promise<Qrl> { + // NB: This can return null if the qrl does not exist, you need to handle it + return await this.qrlModel.findById(id); + } + async getQrlByQuestion(question: string): Promise<Qrl> { + // NB: This can return null if the qrl does not exist, you need to handle it + return await this.qrlModel.findOne({ question: question }); + } + + async addQrl(qrl: CreateQrlDto): Promise<void> { + if (!this.validateQrl(qrl)) { + return Promise.reject('Invalid qrl'); + } + const existingQrl = await this.getQrlByQuestion(qrl.question); + if(existingQrl){ + return Promise.reject('Qrl already exists'); + } + const currentDate = this.dateService.currentTime(); + const newQrl = { ...qrl, lastModified: currentDate }; + try { + await this.qrlModel.create(newQrl); + } catch (error) { + return Promise.reject(`Failed to insert course: ${error}`); + } + } + + async deleteQrl(question: string): Promise<void> { + try { + const res = await this.qrlModel.deleteOne({ question: question }); + if (res.deletedCount === 0) { + return Promise.reject('Could not find course'); + } + } catch (error) { + return Promise.reject(`Failed to delete course: ${error}`); + } + } + + async modifyQrl(qrl: UpdateQrlDto): Promise<void> { + const currentDate = this.dateService.currentTime(); + const filterQuery = { question: qrl.question }; + const updatedQrl = { ...qrl, lastModified: currentDate }; + // Can also use replaceOne if we want to replace the entire object + try { + const qcmFound = await this.getQrlByQuestion(qrl.question); + if(!qcmFound){ + return Promise.reject('Qcm does not exist'); + } + if(qcmFound && qrl._id !== qcmFound._id ){ + return Promise.reject('Another qcm already has the same question'); + } + const res = await this.qrlModel.replaceOne(filterQuery, updatedQrl); + if (res.matchedCount === 0) { + return Promise.reject('Could not find qrl'); + } + } catch (error) { + return Promise.reject(`Failed to update qrl: ${error}`); + } + } + + async getQrlQuestion(question: string): Promise<string> { + const filterQuery = { question: question }; + // Only get the choices and not any of the other fields + try { + const res = await this.qrlModel.findOne(filterQuery, {question: 1}); + return res.question; + } catch (error) { + return Promise.reject(`Failed to get data: ${error}`); + } + } + + private validateQrl(qrl: CreateQrlDto): boolean { + return this.validatePoints(qrl.points); + } + private validatePoints(points: number): boolean { + return points % 10 === 0 && points >= 10 && points <= 100; + } +} -- GitLab From 19c7102352ce04da730cbc0d1bf0e5035365d940 Mon Sep 17 00:00:00 2001 From: Caaf14 <cata-araya@outlook.com> Date: Sun, 21 Jan 2024 22:28:57 -0500 Subject: [PATCH 2/7] remove magic number --- server/app/services/qcm/qcm.service.spec.ts | 18 +----------------- server/app/services/qcm/qcm.service.ts | 11 +++-------- server/app/services/qrl/qrl.service.spec.ts | 18 +----------------- server/app/services/qrl/qrl.service.ts | 19 +++++++------------ 4 files changed, 12 insertions(+), 54 deletions(-) diff --git a/server/app/services/qcm/qcm.service.spec.ts b/server/app/services/qcm/qcm.service.spec.ts index b254c933..ee615586 100644 --- a/server/app/services/qcm/qcm.service.spec.ts +++ b/server/app/services/qcm/qcm.service.spec.ts @@ -8,22 +8,12 @@ import { QcmService } from './qcm.service'; import { Qcm, QcmDocument, qcmSchema } from '@app/model/database/qcm'; import { MongooseModule, getConnectionToken, getModelToken } from '@nestjs/mongoose'; -/** - * There is two way to test the service : - * - Mock the mongoose Model implementation and do what ever we want to do with it (see describe QcmService) or - * - Use mongodb memory server implementation (see describe QcmServiceEndToEnd) and let everything go through as if we had a real database - * - * The second method is generally better because it tests the database queries too. - * We will use it more - */ - describe('QcmService', () => { let service: QcmService; let qcmModel: Model<QcmDocument>; beforeEach(async () => { - // notice that only the functions we call from the model are mocked - // we can´t use sinon because mongoose Model is an interface + qcmModel = { countDocuments: jest.fn(), insertMany: jest.fn(), @@ -79,8 +69,6 @@ describe('QcmServiceEndToEnd', () => { beforeEach(async () => { mongoServer = await MongoMemoryServer.create(); - // notice that only the functions we call from the model are mocked - // we can´t use sinon because mongoose Model is an interface const module = await Test.createTestingModule({ imports: [ MongooseModule.forRootAsync({ @@ -99,9 +87,6 @@ describe('QcmServiceEndToEnd', () => { }); afterEach((done) => { - // The database get auto populated in the constructor - // We want to make sur we close the connection after the database got - // populated. So we add small delay setTimeout(async () => { await connection.close(); await mongoServer.stop(); @@ -132,7 +117,6 @@ describe('QcmServiceEndToEnd', () => { const eltCountsBefore = await qcmModel.countDocuments(); await service.populateDB(); const eltCountsAfter = await qcmModel.countDocuments(); - // eslint-disable-next-line @typescript-eslint/no-magic-numbers expect(eltCountsAfter - eltCountsBefore).toEqual(2); }); diff --git a/server/app/services/qcm/qcm.service.ts b/server/app/services/qcm/qcm.service.ts index a4dbf8bf..0c0cef09 100644 --- a/server/app/services/qcm/qcm.service.ts +++ b/server/app/services/qcm/qcm.service.ts @@ -4,11 +4,10 @@ import { Model } from 'mongoose'; import { Choice, Qcm, QcmDocument } from '@app/model/database/qcm'; import { CreateQcmDto } from '@app/model/dto/qcm/create-qcm.dto'; +import { QCM_MAX_POINTS, QCM_MIN_CHOICES, QCM_MIN_POINTS } from '@app/model/dto/qcm/qcm.dto.constants'; import { UpdateQcmDto } from '@app/model/dto/qcm/update-qcm.dto'; import { DateService } from '@app/services/date/date.service'; -const MAXIMUM_NUMBER_OF_CREDITS = 6; - @Injectable() export class QcmService { constructor( @@ -58,7 +57,6 @@ export class QcmService { } async getQcmByQuestion(question: string): Promise<Qcm> { - // NB: This can return null if the qcm does not exist, you need to handle it return await this.qcmModel.findOne({ question: question }); } @@ -95,7 +93,6 @@ export class QcmService { const currentDate = this.dateService.currentTime(); const filterQuery = { id: qcm._id }; const updatedQcm = { ...qcm, lastModified: currentDate }; - // Can also use replaceOne if we want to replace the entire object try { const qcmFound = await this.getQcmByQuestion(qcm.question); if(!qcmFound){ @@ -115,7 +112,6 @@ export class QcmService { async getQcmQuestion(question: string): Promise<string> { const filterQuery = { question: question }; - // Only get the choices and not any of the other fields try { const res = await this.qcmModel.findOne(filterQuery, {question: 1}); return res.question; @@ -125,7 +121,6 @@ export class QcmService { } async getQcmChoices(question: string): Promise<Array<Choice>> { const filterQuery = { question: question }; - // Only get the choices and not any of the other fields try { const res = await this.qcmModel.findOne(filterQuery, {choices: 1}); return res.choices; @@ -138,7 +133,7 @@ export class QcmService { return this.validatePoints(qcm.points) && this.validateChoices(qcm.choices); } private validatePoints(points: number): boolean { - return points % 10 === 0 && points >= 10 && points <= 100; + return points % 10 === 0 && points >= QCM_MIN_POINTS && points <= QCM_MAX_POINTS; } private validateChoices(choices: Choice[]): boolean { let correctChoices = 0; @@ -151,6 +146,6 @@ export class QcmService { } }); let sumChoices = correctChoices + incorrectChoices; - return (correctChoices >= 1 && incorrectChoices >= 1) && (sumChoices == 4 || sumChoices == 2); + return (correctChoices >= 1 && incorrectChoices >= 1) && (sumChoices == QCM_MIN_CHOICES || sumChoices == QCM_MAX_POINTS); } } diff --git a/server/app/services/qrl/qrl.service.spec.ts b/server/app/services/qrl/qrl.service.spec.ts index 7432290b..236b6b1d 100644 --- a/server/app/services/qrl/qrl.service.spec.ts +++ b/server/app/services/qrl/qrl.service.spec.ts @@ -8,22 +8,12 @@ import { QrlService } from './qrl.service'; import { Qrl, QrlDocument, qrlSchema } from '@app/model/database/qrl'; import { MongooseModule, getConnectionToken, getModelToken } from '@nestjs/mongoose'; -/** - * There is two way to test the service : - * - Mock the mongoose Model implementation and do what ever we want to do with it (see describe QrlService) or - * - Use mongodb memory server implementation (see describe QrlServiceEndToEnd) and let everything go through as if we had a real database - * - * The second method is generally better because it tests the database queries too. - * We will use it more - */ - describe('QrlService', () => { let service: QrlService; let qrlModel: Model<QrlDocument>; beforeEach(async () => { - // notice that only the functions we call from the model are mocked - // we can´t use sinon because mongoose Model is an interface + qrlModel = { countDocuments: jest.fn(), insertMany: jest.fn(), @@ -79,8 +69,6 @@ describe('QrlServiceEndToEnd', () => { beforeEach(async () => { mongoServer = await MongoMemoryServer.create(); - // notice that only the functions we call from the model are mocked - // we can´t use sinon because mongoose Model is an interface const module = await Test.createTestingModule({ imports: [ MongooseModule.forRootAsync({ @@ -99,9 +87,6 @@ describe('QrlServiceEndToEnd', () => { }); afterEach((done) => { - // The database get auto populated in the constructor - // We want to make sur we close the connection after the database got - // populated. So we add small delay setTimeout(async () => { await connection.close(); await mongoServer.stop(); @@ -132,7 +117,6 @@ describe('QrlServiceEndToEnd', () => { const eltCountsBefore = await qrlModel.countDocuments(); await service.populateDB(); const eltCountsAfter = await qrlModel.countDocuments(); - // eslint-disable-next-line @typescript-eslint/no-magic-numbers expect(eltCountsAfter - eltCountsBefore).toEqual(2); }); diff --git a/server/app/services/qrl/qrl.service.ts b/server/app/services/qrl/qrl.service.ts index dc9afaf6..541a0339 100644 --- a/server/app/services/qrl/qrl.service.ts +++ b/server/app/services/qrl/qrl.service.ts @@ -4,11 +4,10 @@ import { Model } from 'mongoose'; import { Qrl, QrlDocument } from '@app/model/database/qrl'; import { CreateQrlDto } from '@app/model/dto/qrl/create-qrl.dto'; +import { QRL_MAX_POINTS, QRL_MIN_POINTS } from '@app/model/dto/qrl/qrl.dto.constants'; import { UpdateQrlDto } from '@app/model/dto/qrl/update-qrl.dto'; import { DateService } from '@app/services/date/date.service'; -const MAXIMUM_NUMBER_OF_CREDITS = 6; - @Injectable() export class QrlService { constructor( @@ -48,11 +47,9 @@ export class QrlService { } async getQrlById(id: string): Promise<Qrl> { - // NB: This can return null if the qrl does not exist, you need to handle it return await this.qrlModel.findById(id); } async getQrlByQuestion(question: string): Promise<Qrl> { - // NB: This can return null if the qrl does not exist, you need to handle it return await this.qrlModel.findOne({ question: question }); } @@ -88,14 +85,13 @@ export class QrlService { const currentDate = this.dateService.currentTime(); const filterQuery = { question: qrl.question }; const updatedQrl = { ...qrl, lastModified: currentDate }; - // Can also use replaceOne if we want to replace the entire object try { - const qcmFound = await this.getQrlByQuestion(qrl.question); - if(!qcmFound){ - return Promise.reject('Qcm does not exist'); + const qrlFound = await this.getQrlByQuestion(qrl.question); + if(!qrlFound){ + return Promise.reject('Qrl does not exist'); } - if(qcmFound && qrl._id !== qcmFound._id ){ - return Promise.reject('Another qcm already has the same question'); + if(qrlFound && qrl._id !== qrlFound._id ){ + return Promise.reject('Another qrl already has the same question'); } const res = await this.qrlModel.replaceOne(filterQuery, updatedQrl); if (res.matchedCount === 0) { @@ -108,7 +104,6 @@ export class QrlService { async getQrlQuestion(question: string): Promise<string> { const filterQuery = { question: question }; - // Only get the choices and not any of the other fields try { const res = await this.qrlModel.findOne(filterQuery, {question: 1}); return res.question; @@ -121,6 +116,6 @@ export class QrlService { return this.validatePoints(qrl.points); } private validatePoints(points: number): boolean { - return points % 10 === 0 && points >= 10 && points <= 100; + return points % 10 === 0 && points >= QRL_MIN_POINTS && points <= QRL_MAX_POINTS; } } -- GitLab From ddb2073e575b993ea1d262fbcf797cb8f5a0d0dd Mon Sep 17 00:00:00 2001 From: Caaf14 <cata-araya@outlook.com> Date: Sun, 21 Jan 2024 22:37:02 -0500 Subject: [PATCH 3/7] remove forgotten magic numbers --- server/app/model/dto/qcm/qcm.dto.constants.ts | 5 ++++- server/app/model/dto/qrl/qrl.dto.constants.ts | 3 ++- server/app/services/qcm/qcm.service.ts | 6 +++--- server/app/services/qrl/qrl.service.ts | 4 ++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/server/app/model/dto/qcm/qcm.dto.constants.ts b/server/app/model/dto/qcm/qcm.dto.constants.ts index b0968f76..e9922869 100644 --- a/server/app/model/dto/qcm/qcm.dto.constants.ts +++ b/server/app/model/dto/qcm/qcm.dto.constants.ts @@ -1,4 +1,7 @@ export const QCM_MAX_POINTS = 100; export const QCM_MIN_POINTS = 10; export const QCM_MAX_CHOICES = 4; -export const QCM_MIN_CHOICES = 2; \ No newline at end of file +export const QCM_MIN_CHOICES = 2; +export const QCM_POINT_DIV_FACTOR = 10; +export const QCM_MIN_CORRECT_ANSWER = 1; +export const QCM_MIN_WRONG_ANSWER = 1; \ No newline at end of file diff --git a/server/app/model/dto/qrl/qrl.dto.constants.ts b/server/app/model/dto/qrl/qrl.dto.constants.ts index c7c1155a..c153a3f0 100644 --- a/server/app/model/dto/qrl/qrl.dto.constants.ts +++ b/server/app/model/dto/qrl/qrl.dto.constants.ts @@ -1,2 +1,3 @@ export const QRL_MAX_POINTS = 100; -export const QRL_MIN_POINTS = 10; \ No newline at end of file +export const QRL_MIN_POINTS = 10; +export const QRL_POINT_DIV_FACTOR = 10; \ No newline at end of file diff --git a/server/app/services/qcm/qcm.service.ts b/server/app/services/qcm/qcm.service.ts index 0c0cef09..769538e7 100644 --- a/server/app/services/qcm/qcm.service.ts +++ b/server/app/services/qcm/qcm.service.ts @@ -4,7 +4,7 @@ import { Model } from 'mongoose'; import { Choice, Qcm, QcmDocument } from '@app/model/database/qcm'; import { CreateQcmDto } from '@app/model/dto/qcm/create-qcm.dto'; -import { QCM_MAX_POINTS, QCM_MIN_CHOICES, QCM_MIN_POINTS } from '@app/model/dto/qcm/qcm.dto.constants'; +import { QCM_MAX_POINTS, QCM_MIN_CHOICES, QCM_MIN_CORRECT_ANSWER, QCM_MIN_POINTS, QCM_MIN_WRONG_ANSWER, QCM_POINT_DIV_FACTOR } from '@app/model/dto/qcm/qcm.dto.constants'; import { UpdateQcmDto } from '@app/model/dto/qcm/update-qcm.dto'; import { DateService } from '@app/services/date/date.service'; @@ -133,7 +133,7 @@ export class QcmService { return this.validatePoints(qcm.points) && this.validateChoices(qcm.choices); } private validatePoints(points: number): boolean { - return points % 10 === 0 && points >= QCM_MIN_POINTS && points <= QCM_MAX_POINTS; + return points % QCM_POINT_DIV_FACTOR === 0 && points >= QCM_MIN_POINTS && points <= QCM_MAX_POINTS; } private validateChoices(choices: Choice[]): boolean { let correctChoices = 0; @@ -146,6 +146,6 @@ export class QcmService { } }); let sumChoices = correctChoices + incorrectChoices; - return (correctChoices >= 1 && incorrectChoices >= 1) && (sumChoices == QCM_MIN_CHOICES || sumChoices == QCM_MAX_POINTS); + return (correctChoices >= QCM_MIN_CORRECT_ANSWER && incorrectChoices >= QCM_MIN_WRONG_ANSWER) && (sumChoices == QCM_MIN_CHOICES || sumChoices == QCM_MAX_POINTS); } } diff --git a/server/app/services/qrl/qrl.service.ts b/server/app/services/qrl/qrl.service.ts index 541a0339..64c4b8de 100644 --- a/server/app/services/qrl/qrl.service.ts +++ b/server/app/services/qrl/qrl.service.ts @@ -4,7 +4,7 @@ import { Model } from 'mongoose'; import { Qrl, QrlDocument } from '@app/model/database/qrl'; import { CreateQrlDto } from '@app/model/dto/qrl/create-qrl.dto'; -import { QRL_MAX_POINTS, QRL_MIN_POINTS } from '@app/model/dto/qrl/qrl.dto.constants'; +import { QRL_MAX_POINTS, QRL_MIN_POINTS, QRL_POINT_DIV_FACTOR } from '@app/model/dto/qrl/qrl.dto.constants'; import { UpdateQrlDto } from '@app/model/dto/qrl/update-qrl.dto'; import { DateService } from '@app/services/date/date.service'; @@ -116,6 +116,6 @@ export class QrlService { return this.validatePoints(qrl.points); } private validatePoints(points: number): boolean { - return points % 10 === 0 && points >= QRL_MIN_POINTS && points <= QRL_MAX_POINTS; + return points % QRL_POINT_DIV_FACTOR === 0 && points >= QRL_MIN_POINTS && points <= QRL_MAX_POINTS; } } -- GitLab From edd018448e5c1bfbba044813d0a7c9e94e141aa7 Mon Sep 17 00:00:00 2001 From: Caaf14 <cata-araya@outlook.com> Date: Sun, 21 Jan 2024 23:24:36 -0500 Subject: [PATCH 4/7] fix lint --- server/app/model/database/choice.ts | 4 + server/app/model/database/qcm.ts | 6 +- server/app/model/dto/qcm/create-qcm.dto.ts | 4 +- server/app/model/dto/qcm/update-qcm.dto.ts | 4 +- server/app/model/dto/qrl/create-qrl.dto.ts | 2 +- server/app/model/dto/qrl/qrl.dto.constants.ts | 2 +- server/app/services/qcm/qcm.service.spec.ts | 79 +++++++++++++------ server/app/services/qcm/qcm.service.ts | 39 +++++---- server/app/services/qrl/qrl.service.spec.ts | 6 +- server/app/services/qrl/qrl.service.ts | 10 +-- 10 files changed, 101 insertions(+), 55 deletions(-) create mode 100644 server/app/model/database/choice.ts diff --git a/server/app/model/database/choice.ts b/server/app/model/database/choice.ts new file mode 100644 index 00000000..9f357da8 --- /dev/null +++ b/server/app/model/database/choice.ts @@ -0,0 +1,4 @@ +export class Choice { + choice: string; + correct: boolean; +} \ No newline at end of file diff --git a/server/app/model/database/qcm.ts b/server/app/model/database/qcm.ts index 9af8c336..72dcec33 100644 --- a/server/app/model/database/qcm.ts +++ b/server/app/model/database/qcm.ts @@ -1,14 +1,10 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { ApiProperty } from '@nestjs/swagger'; import { Document } from 'mongoose'; +import { Choice } from './choice'; export type QcmDocument = Qcm & Document; -export class Choice { - choice: string; - correct: boolean; - } - @Schema() export class Qcm { @ApiProperty() diff --git a/server/app/model/dto/qcm/create-qcm.dto.ts b/server/app/model/dto/qcm/create-qcm.dto.ts index d39f4ae8..a389632f 100644 --- a/server/app/model/dto/qcm/create-qcm.dto.ts +++ b/server/app/model/dto/qcm/create-qcm.dto.ts @@ -1,4 +1,4 @@ -import { Choice } from '@app/model/database/qcm'; +import { Choice } from '@app/model/database/choice'; import { QCM_MAX_CHOICES, QCM_MAX_POINTS, QCM_MIN_CHOICES, QCM_MIN_POINTS } from '@app/model/dto/qcm/qcm.dto.constants'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; @@ -26,5 +26,5 @@ export class CreateQcmDto { @ApiProperty() @IsString() lastModified?: string; - + } diff --git a/server/app/model/dto/qcm/update-qcm.dto.ts b/server/app/model/dto/qcm/update-qcm.dto.ts index d494e1b0..86de8b56 100644 --- a/server/app/model/dto/qcm/update-qcm.dto.ts +++ b/server/app/model/dto/qcm/update-qcm.dto.ts @@ -1,4 +1,4 @@ -import { Choice } from '@app/model/database/qcm'; +import { Choice } from '@app/model/database/choice'; import { QCM_MAX_CHOICES, QCM_MAX_POINTS, QCM_MIN_CHOICES, QCM_MIN_POINTS } from '@app/model/dto/qcm/qcm.dto.constants'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; @@ -30,5 +30,5 @@ export class UpdateQcmDto { @IsNotEmpty() @IsString() _id?: string; - + } diff --git a/server/app/model/dto/qrl/create-qrl.dto.ts b/server/app/model/dto/qrl/create-qrl.dto.ts index 87cc4d40..ae00a276 100644 --- a/server/app/model/dto/qrl/create-qrl.dto.ts +++ b/server/app/model/dto/qrl/create-qrl.dto.ts @@ -16,5 +16,5 @@ export class CreateQrlDto { @ApiProperty() @IsString() lastModified: string; - + } diff --git a/server/app/model/dto/qrl/qrl.dto.constants.ts b/server/app/model/dto/qrl/qrl.dto.constants.ts index c153a3f0..5a240dc0 100644 --- a/server/app/model/dto/qrl/qrl.dto.constants.ts +++ b/server/app/model/dto/qrl/qrl.dto.constants.ts @@ -1,3 +1,3 @@ export const QRL_MAX_POINTS = 100; export const QRL_MIN_POINTS = 10; -export const QRL_POINT_DIV_FACTOR = 10; \ No newline at end of file +export const QRL_POINT_DIV_FACTOR = 10; diff --git a/server/app/services/qcm/qcm.service.spec.ts b/server/app/services/qcm/qcm.service.spec.ts index ee615586..3ceb16e5 100644 --- a/server/app/services/qcm/qcm.service.spec.ts +++ b/server/app/services/qcm/qcm.service.spec.ts @@ -208,7 +208,6 @@ describe('QcmServiceEndToEnd', () => { it('addQcm() should fail if question already is in DB', async () => { await qcmModel.deleteMany({}); const qcm = getFakeQcm(); - await qcmModel.create(qcm); await expect(service.addQcm(qcm)).rejects.toBeTruthy(); expect(await qcmModel.countDocuments()).toEqual(1); @@ -221,28 +220,64 @@ describe('QcmServiceEndToEnd', () => { }); it('addQcm() should fail if the qcm is not a valid', async () => { + await qcmModel.deleteMany({}); const qcm = getFakeQcm(); - await expect(service.addQcm({ ...qcm, question: 'Question1', points: 35,choices: [ - { choice: 'Oracles', correct: true }, - { choice: 'Oracle', correct: false }, - ] })).rejects.toBeTruthy(); - await expect(service.addQcm({ ...qcm, question: 'Question1', points: 0,choices: [ - { choice: 'Oracles', correct: true }, - { choice: 'Oracle', correct: false }, - ] })).rejects.toBeTruthy(); - await expect(service.addQcm({ ...qcm, question: 'Question1', points: 110,choices: [ - { choice: 'Oracles', correct: true }, - { choice: 'Oracle', correct: false }, - ] })).rejects.toBeTruthy(); - await expect(service.addQcm({ ...qcm, question: 'Question1', points: 50,choices: [ - { choice: 'Oracles', correct: true }, - { choice: 'Oracle', correct: true }, - ] })).rejects.toBeTruthy(); - await expect(service.addQcm({ ...qcm, question: 'Question1', points: 50,choices: [ - { choice: 'Oracles', correct: true }, - { choice: 'Oracle', correct: false }, - { choice: 'Oracle', correct: false }, - ] })).rejects.toBeTruthy(); + await expect( + service.addQcm({ + ...qcm, + question: 'Question1', + points: 35, + choices: [ + { choice: 'Oracles', correct: true }, + { choice: 'Oracle', correct: false }, + ] + }) + ).rejects.toBeTruthy(); + await expect( + service.addQcm({ + ...qcm, + question: 'Question1', + points: 0, + choices: [ + { choice: 'Oracles', correct: true }, + { choice: 'Oracle', correct: false }, + ] + }) + ).rejects.toBeTruthy(); + await expect( + service.addQcm({ + ...qcm, + question: 'Question1', + points: 110, + choices: [ + { choice: 'Oracles', correct: true }, + { choice: 'Oracle', correct: false }, + ] + }) + ).rejects.toBeTruthy(); + await expect( + service.addQcm({ + ...qcm, + question: 'Question1', + points: 50, + choices: [ + { choice: 'Oracles', correct: true }, + { choice: 'Oracle', correct: true }, + ] + }) + ).rejects.toBeTruthy(); + await expect( + service.addQcm({ + ...qcm, + question: 'Question1', + points: 50, + choices: [ + { choice: 'Oracles', correct: true }, + { choice: 'Oracle', correct: false }, + { choice: 'Oracle', correct: false }, + ] + }) + ).rejects.toBeTruthy(); }); }); diff --git a/server/app/services/qcm/qcm.service.ts b/server/app/services/qcm/qcm.service.ts index 769538e7..4d1931dc 100644 --- a/server/app/services/qcm/qcm.service.ts +++ b/server/app/services/qcm/qcm.service.ts @@ -2,9 +2,17 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; -import { Choice, Qcm, QcmDocument } from '@app/model/database/qcm'; +import { Choice } from '@app/model/database/choice'; +import { Qcm, QcmDocument } from '@app/model/database/qcm'; import { CreateQcmDto } from '@app/model/dto/qcm/create-qcm.dto'; -import { QCM_MAX_POINTS, QCM_MIN_CHOICES, QCM_MIN_CORRECT_ANSWER, QCM_MIN_POINTS, QCM_MIN_WRONG_ANSWER, QCM_POINT_DIV_FACTOR } from '@app/model/dto/qcm/qcm.dto.constants'; +import { + QCM_MAX_POINTS, + QCM_MIN_CHOICES, + QCM_MIN_CORRECT_ANSWER, + QCM_MIN_POINTS, + QCM_MIN_WRONG_ANSWER, + QCM_POINT_DIV_FACTOR +} from '@app/model/dto/qcm/qcm.dto.constants'; import { UpdateQcmDto } from '@app/model/dto/qcm/update-qcm.dto'; import { DateService } from '@app/services/date/date.service'; @@ -61,12 +69,11 @@ export class QcmService { } async addQcm(qcm: CreateQcmDto): Promise<void> { - if (!this.validateQcm(qcm)) { return Promise.reject('Invalid qcm'); } const existingQcm = await this.getQcmByQuestion(qcm.question); - if(existingQcm){ + if (existingQcm) { return Promise.reject('Qcm already exists'); } const currentDate = this.dateService.currentTime(); @@ -94,11 +101,11 @@ export class QcmService { const filterQuery = { id: qcm._id }; const updatedQcm = { ...qcm, lastModified: currentDate }; try { - const qcmFound = await this.getQcmByQuestion(qcm.question); - if(!qcmFound){ + const qcmFound = await this.getQcmByQuestion(qcm.question); + if (!qcmFound) { return Promise.reject('Qcm does not exist'); - } - if(qcmFound && qcm._id !== qcmFound._id ){ + } + if (qcmFound && qcm._id !== qcmFound._id) { return Promise.reject('Another qcm already has the same question'); } const res = await this.qcmModel.replaceOne(filterQuery, updatedQcm); @@ -113,16 +120,16 @@ export class QcmService { async getQcmQuestion(question: string): Promise<string> { const filterQuery = { question: question }; try { - const res = await this.qcmModel.findOne(filterQuery, {question: 1}); + const res = await this.qcmModel.findOne(filterQuery, { question: 1 }); return res.question; } catch (error) { return Promise.reject(`Failed to get data: ${error}`); } } - async getQcmChoices(question: string): Promise<Array<Choice>> { + async getQcmChoices(question: string): Promise<Choice[]> { const filterQuery = { question: question }; try { - const res = await this.qcmModel.findOne(filterQuery, {choices: 1}); + const res = await this.qcmModel.findOne(filterQuery, { choices: 1 }); return res.choices; } catch (error) { return Promise.reject(`Failed to get data: ${error}`); @@ -138,14 +145,18 @@ export class QcmService { private validateChoices(choices: Choice[]): boolean { let correctChoices = 0; let incorrectChoices = 0; - choices.forEach(choice => { + choices.forEach((choice) => { if (choice.correct) { correctChoices++; } else { incorrectChoices++; } }); - let sumChoices = correctChoices + incorrectChoices; - return (correctChoices >= QCM_MIN_CORRECT_ANSWER && incorrectChoices >= QCM_MIN_WRONG_ANSWER) && (sumChoices == QCM_MIN_CHOICES || sumChoices == QCM_MAX_POINTS); + const sumChoices = correctChoices + incorrectChoices; + return ( + correctChoices >= QCM_MIN_CORRECT_ANSWER && + incorrectChoices >= QCM_MIN_WRONG_ANSWER && + (sumChoices === QCM_MIN_CHOICES || sumChoices === QCM_MAX_POINTS) + ); } } diff --git a/server/app/services/qrl/qrl.service.spec.ts b/server/app/services/qrl/qrl.service.spec.ts index 236b6b1d..78ce7edd 100644 --- a/server/app/services/qrl/qrl.service.spec.ts +++ b/server/app/services/qrl/qrl.service.spec.ts @@ -209,9 +209,9 @@ describe('QrlServiceEndToEnd', () => { it('addQrl() should fail if the qrl is not a valid', async () => { const qrl = getFakeQrl(); - await expect(service.addQrl({ ...qrl, question: 'Question1', points: 35})).rejects.toBeTruthy(); - await expect(service.addQrl({ ...qrl, question: 'Question1', points: 0})).rejects.toBeTruthy(); - await expect(service.addQrl({ ...qrl, question: 'Question1', points: 110})).rejects.toBeTruthy(); + await expect(service.addQrl({ ...qrl, question: 'Question1', points: 35 })).rejects.toBeTruthy(); + await expect(service.addQrl({ ...qrl, question: 'Question1', points: 0 })).rejects.toBeTruthy(); + await expect(service.addQrl({ ...qrl, question: 'Question1', points: 110 })).rejects.toBeTruthy(); }); }); diff --git a/server/app/services/qrl/qrl.service.ts b/server/app/services/qrl/qrl.service.ts index 64c4b8de..7fcdb99a 100644 --- a/server/app/services/qrl/qrl.service.ts +++ b/server/app/services/qrl/qrl.service.ts @@ -58,7 +58,7 @@ export class QrlService { return Promise.reject('Invalid qrl'); } const existingQrl = await this.getQrlByQuestion(qrl.question); - if(existingQrl){ + if (existingQrl) { return Promise.reject('Qrl already exists'); } const currentDate = this.dateService.currentTime(); @@ -86,11 +86,11 @@ export class QrlService { const filterQuery = { question: qrl.question }; const updatedQrl = { ...qrl, lastModified: currentDate }; try { - const qrlFound = await this.getQrlByQuestion(qrl.question); - if(!qrlFound){ + const qrlFound = await this.getQrlByQuestion(qrl.question); + if (!qrlFound) { return Promise.reject('Qrl does not exist'); } - if(qrlFound && qrl._id !== qrlFound._id ){ + if (qrlFound && qrl._id !== qrlFound._id) { return Promise.reject('Another qrl already has the same question'); } const res = await this.qrlModel.replaceOne(filterQuery, updatedQrl); @@ -105,7 +105,7 @@ export class QrlService { async getQrlQuestion(question: string): Promise<string> { const filterQuery = { question: question }; try { - const res = await this.qrlModel.findOne(filterQuery, {question: 1}); + const res = await this.qrlModel.findOne(filterQuery, { question: 1 }); return res.question; } catch (error) { return Promise.reject(`Failed to get data: ${error}`); -- GitLab From 898a197939204b60e662ae441fbe62bd1ec54c3b Mon Sep 17 00:00:00 2001 From: Caaf14 <cata-araya@outlook.com> Date: Mon, 22 Jan 2024 18:35:09 -0500 Subject: [PATCH 5/7] rename class choice to choices --- server/app/model/database/{choice.ts => choices.ts} | 2 +- server/app/model/database/qcm.ts | 4 ++-- server/app/model/dto/qcm/create-qcm.dto.ts | 8 ++++---- server/app/model/dto/qcm/update-qcm.dto.ts | 8 ++++---- server/app/services/qcm/qcm.service.ts | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) rename server/app/model/database/{choice.ts => choices.ts} (65%) diff --git a/server/app/model/database/choice.ts b/server/app/model/database/choices.ts similarity index 65% rename from server/app/model/database/choice.ts rename to server/app/model/database/choices.ts index 9f357da8..d5e2bf3e 100644 --- a/server/app/model/database/choice.ts +++ b/server/app/model/database/choices.ts @@ -1,4 +1,4 @@ -export class Choice { +export class Choices { choice: string; correct: boolean; } \ No newline at end of file diff --git a/server/app/model/database/qcm.ts b/server/app/model/database/qcm.ts index 72dcec33..1c57fc55 100644 --- a/server/app/model/database/qcm.ts +++ b/server/app/model/database/qcm.ts @@ -1,7 +1,7 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { ApiProperty } from '@nestjs/swagger'; import { Document } from 'mongoose'; -import { Choice } from './choice'; +import { Choices } from './choices'; export type QcmDocument = Qcm & Document; @@ -17,7 +17,7 @@ export class Qcm { @ApiProperty() @Prop({ required: true }) - choices: Choice[]; + choices: Choices[]; @ApiProperty() @Prop({ required: true }) diff --git a/server/app/model/dto/qcm/create-qcm.dto.ts b/server/app/model/dto/qcm/create-qcm.dto.ts index a389632f..6282adfa 100644 --- a/server/app/model/dto/qcm/create-qcm.dto.ts +++ b/server/app/model/dto/qcm/create-qcm.dto.ts @@ -1,4 +1,4 @@ -import { Choice } from '@app/model/database/choice'; +import { Choices } from '@app/model/database/choices'; import { QCM_MAX_CHOICES, QCM_MAX_POINTS, QCM_MIN_CHOICES, QCM_MIN_POINTS } from '@app/model/dto/qcm/qcm.dto.constants'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; @@ -15,13 +15,13 @@ export class CreateQcmDto { @Max(QCM_MAX_POINTS) points: number; - @ApiProperty({ type: () => Choice, isArray: true }) + @ApiProperty({ type: () => Choices, isArray: true }) @IsArray() @ArrayMinSize(QCM_MIN_CHOICES) @ArrayMaxSize(QCM_MAX_CHOICES) @ValidateNested({ each: true }) - @Type(() => Choice) - choices: Choice[]; + @Type(() => Choices) + choices: Choices[]; @ApiProperty() @IsString() diff --git a/server/app/model/dto/qcm/update-qcm.dto.ts b/server/app/model/dto/qcm/update-qcm.dto.ts index 86de8b56..eec9a56a 100644 --- a/server/app/model/dto/qcm/update-qcm.dto.ts +++ b/server/app/model/dto/qcm/update-qcm.dto.ts @@ -1,4 +1,4 @@ -import { Choice } from '@app/model/database/choice'; +import { Choices } from '@app/model/database/choices'; import { QCM_MAX_CHOICES, QCM_MAX_POINTS, QCM_MIN_CHOICES, QCM_MIN_POINTS } from '@app/model/dto/qcm/qcm.dto.constants'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; @@ -17,14 +17,14 @@ export class UpdateQcmDto { @Max(QCM_MAX_POINTS) points?: number; - @ApiProperty({ type: () => Choice, isArray: true, required: false }) + @ApiProperty({ type: () => Choices, isArray: true, required: false }) @IsOptional() @IsArray() @ArrayMinSize(QCM_MIN_CHOICES) @ArrayMaxSize(QCM_MAX_CHOICES) @ValidateNested({ each: true }) - @Type(() => Choice) - choices?: Choice[]; + @Type(() => Choices) + choices?: Choices[]; @ApiProperty() @IsNotEmpty() diff --git a/server/app/services/qcm/qcm.service.ts b/server/app/services/qcm/qcm.service.ts index 4d1931dc..e0ce3f43 100644 --- a/server/app/services/qcm/qcm.service.ts +++ b/server/app/services/qcm/qcm.service.ts @@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; -import { Choice } from '@app/model/database/choice'; +import { Choices } from '@app/model/database/choices'; import { Qcm, QcmDocument } from '@app/model/database/qcm'; import { CreateQcmDto } from '@app/model/dto/qcm/create-qcm.dto'; import { @@ -126,7 +126,7 @@ export class QcmService { return Promise.reject(`Failed to get data: ${error}`); } } - async getQcmChoices(question: string): Promise<Choice[]> { + async getQcmChoices(question: string): Promise<Choices[]> { const filterQuery = { question: question }; try { const res = await this.qcmModel.findOne(filterQuery, { choices: 1 }); @@ -142,7 +142,7 @@ export class QcmService { private validatePoints(points: number): boolean { return points % QCM_POINT_DIV_FACTOR === 0 && points >= QCM_MIN_POINTS && points <= QCM_MAX_POINTS; } - private validateChoices(choices: Choice[]): boolean { + private validateChoices(choices: Choices[]): boolean { let correctChoices = 0; let incorrectChoices = 0; choices.forEach((choice) => { -- GitLab From 4c6482e8711d07c86c076c54a3ec93c583e40f90 Mon Sep 17 00:00:00 2001 From: Laurent Bourgon <laurent.bourgon@polymtl.ca> Date: Thu, 25 Jan 2024 15:09:30 -0500 Subject: [PATCH 6/7] add game and its dto --- common/game.ts | 12 + common/question.ts | 34 ++ server/app/app.module.ts | 2 + .../dto/abstract-question.dto.constants.ts | 2 + server/app/game/dto/abstract-question.dto.ts | 23 ++ server/app/game/dto/choice.dto.ts | 15 + server/app/game/dto/game.dto.constants.ts | 3 + server/app/game/dto/game.dto.ts | 55 ++++ server/app/game/dto/qcm.dto.constants.ts | 2 + server/app/game/dto/qcm.dto.ts | 21 ++ server/app/game/dto/qrl.dto.ts | 10 + server/app/game/game.module.ts | 15 + .../game/schemas/abstract-question.schema.ts | 16 + server/app/game/schemas/game.schema.ts | 28 ++ server/app/game/schemas/qcm.schema.ts | 16 + server/app/game/schemas/qrl.schema.ts | 13 + server/app/model/dto/qcm/create-qcm.dto.ts | 30 -- server/app/model/dto/qcm/qcm.dto.constants.ts | 7 - server/app/model/dto/qcm/update-qcm.dto.ts | 34 -- server/app/model/dto/qrl/create-qrl.dto.ts | 20 -- server/app/model/dto/qrl/qrl.dto.constants.ts | 3 - server/app/model/dto/qrl/update-qrl.dto.ts | 26 -- server/app/services/qcm/qcm.service.spec.ts | 304 ------------------ server/app/services/qcm/qcm.service.ts | 162 ---------- server/app/services/qrl/qrl.service.spec.ts | 230 ------------- server/app/services/qrl/qrl.service.ts | 121 ------- 26 files changed, 267 insertions(+), 937 deletions(-) create mode 100644 common/game.ts create mode 100644 common/question.ts create mode 100644 server/app/game/dto/abstract-question.dto.constants.ts create mode 100644 server/app/game/dto/abstract-question.dto.ts create mode 100644 server/app/game/dto/choice.dto.ts create mode 100644 server/app/game/dto/game.dto.constants.ts create mode 100644 server/app/game/dto/game.dto.ts create mode 100644 server/app/game/dto/qcm.dto.constants.ts create mode 100644 server/app/game/dto/qcm.dto.ts create mode 100644 server/app/game/dto/qrl.dto.ts create mode 100644 server/app/game/game.module.ts create mode 100644 server/app/game/schemas/abstract-question.schema.ts create mode 100644 server/app/game/schemas/game.schema.ts create mode 100644 server/app/game/schemas/qcm.schema.ts create mode 100644 server/app/game/schemas/qrl.schema.ts delete mode 100644 server/app/model/dto/qcm/create-qcm.dto.ts delete mode 100644 server/app/model/dto/qcm/qcm.dto.constants.ts delete mode 100644 server/app/model/dto/qcm/update-qcm.dto.ts delete mode 100644 server/app/model/dto/qrl/create-qrl.dto.ts delete mode 100644 server/app/model/dto/qrl/qrl.dto.constants.ts delete mode 100644 server/app/model/dto/qrl/update-qrl.dto.ts delete mode 100644 server/app/services/qcm/qcm.service.spec.ts delete mode 100644 server/app/services/qcm/qcm.service.ts delete mode 100644 server/app/services/qrl/qrl.service.spec.ts delete mode 100644 server/app/services/qrl/qrl.service.ts diff --git a/common/game.ts b/common/game.ts new file mode 100644 index 00000000..a5a9db5e --- /dev/null +++ b/common/game.ts @@ -0,0 +1,12 @@ +import { Qcm, Qrl } from './question'; + +interface BaseGame { + title: string; + description: string; + duration: number; + lastModification: Date; + questions: (Qcm | Qrl)[]; +} + +export type CreateGame = BaseGame; +export type Game = CreateGame & { _id: string }; diff --git a/common/question.ts b/common/question.ts new file mode 100644 index 00000000..52fa59f6 --- /dev/null +++ b/common/question.ts @@ -0,0 +1,34 @@ +export enum QuestionType { + Qcm = 'QCM', + Qrl = 'QRL', +} + +interface AbstractQuestion { + text: string; + points: number; +} + +export interface Choice { + text: string; + isCorrect: boolean; +} + +interface BaseQcm extends AbstractQuestion { + type: QuestionType.Qcm; + choices: Choice[]; +} + +interface BaseQrl extends AbstractQuestion { + type: QuestionType.Qrl; +} + +export type BaseQuestion = BaseQcm | BaseQrl; + +export type CreateQcm = BaseQcm; +export type CreateQrl = BaseQrl; +export type CreateQuestion = CreateQcm | CreateQrl; + +type ConcreteQuestion<T extends BaseQuestion> = T & { _id: string }; +export type Qcm = ConcreteQuestion<CreateQcm>; +export type Qrl = ConcreteQuestion<CreateQrl>; +export type Question = ConcreteQuestion<CreateQuestion>; diff --git a/server/app/app.module.ts b/server/app/app.module.ts index c48fe474..4af2541a 100644 --- a/server/app/app.module.ts +++ b/server/app/app.module.ts @@ -11,11 +11,13 @@ import { ExampleController } from '@app/controllers/example/example.controller'; import { AuthModule } from '@app/auth/auth.module'; import { ConfigModule } from '@app/config/config.module'; import { ConfigService } from '@app/config/config.service'; +import { GameModule } from './game/game.module'; @Module({ imports: [ AuthModule, ConfigModule, + GameModule, MongooseModule.forRootAsync({ inject: [ConfigService], useFactory: async (config: ConfigService) => ({ diff --git a/server/app/game/dto/abstract-question.dto.constants.ts b/server/app/game/dto/abstract-question.dto.constants.ts new file mode 100644 index 00000000..aa590516 --- /dev/null +++ b/server/app/game/dto/abstract-question.dto.constants.ts @@ -0,0 +1,2 @@ +export const MIN_POINTS = 10; +export const MAX_POINTS = 100; diff --git a/server/app/game/dto/abstract-question.dto.ts b/server/app/game/dto/abstract-question.dto.ts new file mode 100644 index 00000000..00cfafcf --- /dev/null +++ b/server/app/game/dto/abstract-question.dto.ts @@ -0,0 +1,23 @@ +import { QuestionType } from '@common/question'; +import { IsDefined, IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; +import { MAX_POINTS, MIN_POINTS } from './abstract-question.dto.constants'; +import { ApiProperty } from '@nestjs/swagger'; + +export abstract class AbstractQuestionDto { + @ApiProperty() + @IsNotEmpty() + @IsEnum(QuestionType) + type: QuestionType; + + @ApiProperty() + @IsNotEmpty() + @IsString() + text: string; + + @ApiProperty() + @IsDefined() + @IsInt() + @Min(MIN_POINTS) + @Max(MAX_POINTS) + points: number; +} diff --git a/server/app/game/dto/choice.dto.ts b/server/app/game/dto/choice.dto.ts new file mode 100644 index 00000000..2d909c96 --- /dev/null +++ b/server/app/game/dto/choice.dto.ts @@ -0,0 +1,15 @@ +import { Choice } from '@common/question'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; + +export class ChoiceDto implements Choice { + @ApiProperty() + @IsNotEmpty() + @IsString() + text: string; + + @ApiProperty() + @IsNotEmpty() + @IsBoolean() + isCorrect: boolean; +} diff --git a/server/app/game/dto/game.dto.constants.ts b/server/app/game/dto/game.dto.constants.ts new file mode 100644 index 00000000..45b54580 --- /dev/null +++ b/server/app/game/dto/game.dto.constants.ts @@ -0,0 +1,3 @@ +export const MIN_DURATION = 10; +export const MAX_DURATION = 60; +export const MIN_QUESTIONS = 1; diff --git a/server/app/game/dto/game.dto.ts b/server/app/game/dto/game.dto.ts new file mode 100644 index 00000000..59aaf0a4 --- /dev/null +++ b/server/app/game/dto/game.dto.ts @@ -0,0 +1,55 @@ +import { ArrayMinSize, IsArray, IsDateString, IsNotEmpty, IsNumber, IsString, Max, Min, ValidateNested } from 'class-validator'; +import { QcmDto } from './qcm.dto'; +import { Type } from 'class-transformer'; +import { QrlDto } from './qrl.dto'; +import { QuestionType } from '@common/question'; +import { MAX_DURATION, MIN_DURATION, MIN_QUESTIONS } from './game.dto.constants'; +import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { AbstractQuestionDto } from './abstract-question.dto'; + +@ApiExtraModels(QcmDto, QrlDto) +export class GameDto { + @ApiProperty() + @IsNotEmpty() + @IsString() + title: string; + + @ApiProperty() + @IsNotEmpty() + @IsString() + description: string; + + @ApiProperty({ minimum: MIN_DURATION, maximum: MAX_DURATION }) + @Min(MIN_DURATION) + @Max(MAX_DURATION) + @IsNumber() + duration: number; + + @ApiProperty() + @IsNotEmpty() + @IsDateString() + lastModification: Date; + + @ApiProperty({ + isArray: true, + type: () => AbstractQuestionDto, + oneOf: [ + { type: 'array', items: { $ref: getSchemaPath(QrlDto) } }, + { type: 'array', items: { $ref: getSchemaPath(QcmDto) } }, + ], + }) + @ValidateNested({ each: true }) + @Type(() => AbstractQuestionDto, { + keepDiscriminatorProperty: true, + discriminator: { + property: 'type', + subTypes: [ + { value: QcmDto, name: QuestionType.Qcm }, + { value: QrlDto, name: QuestionType.Qrl }, + ], + }, + }) + @ArrayMinSize(MIN_QUESTIONS) + @IsArray() + questions: (QcmDto | QrlDto)[]; +} diff --git a/server/app/game/dto/qcm.dto.constants.ts b/server/app/game/dto/qcm.dto.constants.ts new file mode 100644 index 00000000..7eea075a --- /dev/null +++ b/server/app/game/dto/qcm.dto.constants.ts @@ -0,0 +1,2 @@ +export const MIN_CHOICES = 2; +export const MAX_CHOICES = 4; diff --git a/server/app/game/dto/qcm.dto.ts b/server/app/game/dto/qcm.dto.ts new file mode 100644 index 00000000..a1049dd8 --- /dev/null +++ b/server/app/game/dto/qcm.dto.ts @@ -0,0 +1,21 @@ +import { QuestionType } from '@common/question'; +import { AbstractQuestionDto } from './abstract-question.dto'; +import { ArrayMaxSize, ArrayMinSize, Equals, IsArray, ValidateNested } from 'class-validator'; +import { MAX_CHOICES, MIN_CHOICES } from './qcm.dto.constants'; +import { ChoiceDto } from './choice.dto'; +import { Type } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; + +export class QcmDto extends AbstractQuestionDto { + @ApiProperty() + @Equals(QuestionType.Qcm) + type: QuestionType.Qcm; + + @ApiProperty({ isArray: true, type: () => ChoiceDto }) + @ValidateNested({ each: true }) + @Type(() => ChoiceDto) + @ArrayMaxSize(MAX_CHOICES) + @ArrayMinSize(MIN_CHOICES) + @IsArray() + choices: ChoiceDto[]; +} diff --git a/server/app/game/dto/qrl.dto.ts b/server/app/game/dto/qrl.dto.ts new file mode 100644 index 00000000..c5739aab --- /dev/null +++ b/server/app/game/dto/qrl.dto.ts @@ -0,0 +1,10 @@ +import { QuestionType } from '@common/question'; +import { AbstractQuestionDto } from './abstract-question.dto'; +import { Equals } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class QrlDto extends AbstractQuestionDto { + @ApiProperty() + @Equals(QuestionType.Qrl) + type: QuestionType.Qrl; +} diff --git a/server/app/game/game.module.ts b/server/app/game/game.module.ts new file mode 100644 index 00000000..2e932d91 --- /dev/null +++ b/server/app/game/game.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { Game, gameSchema } from './schemas/game.schema'; + +@Module({ + imports: [ + MongooseModule.forFeature([ + { + name: Game.name, + schema: gameSchema, + }, + ]), + ], +}) +export class GameModule {} diff --git a/server/app/game/schemas/abstract-question.schema.ts b/server/app/game/schemas/abstract-question.schema.ts new file mode 100644 index 00000000..e6475dee --- /dev/null +++ b/server/app/game/schemas/abstract-question.schema.ts @@ -0,0 +1,16 @@ +import { Prop, Schema } from '@nestjs/mongoose'; +import { QuestionType } from '@common/question'; + +@Schema() +export abstract class AbstractQuestion { + @Prop({ required: true }) + type: QuestionType; + + @Prop({ required: true }) + text: string; + + @Prop({ required: true }) + points: number; + + _id: string; +} diff --git a/server/app/game/schemas/game.schema.ts b/server/app/game/schemas/game.schema.ts new file mode 100644 index 00000000..aedcecb4 --- /dev/null +++ b/server/app/game/schemas/game.schema.ts @@ -0,0 +1,28 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument } from 'mongoose'; +import { Qcm } from './qcm.schema'; +import { Qrl } from './qrl.schema'; + +@Schema() +export class Game { + @Prop({ required: true }) + title: string; + + @Prop({ required: true }) + description: string; + + @Prop({ required: true }) + duration: number; + + @Prop({ required: true }) + lastModification: Date; + + @Prop({ required: true }) + questions: (Qcm | Qrl)[]; + + _id: string; +} + +export type GameDocument = HydratedDocument<Game>; + +export const gameSchema = SchemaFactory.createForClass(Game); diff --git a/server/app/game/schemas/qcm.schema.ts b/server/app/game/schemas/qcm.schema.ts new file mode 100644 index 00000000..90332270 --- /dev/null +++ b/server/app/game/schemas/qcm.schema.ts @@ -0,0 +1,16 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Choice, Qcm as IQcm, QuestionType } from '@common/question'; +import { AbstractQuestion } from './abstract-question.schema'; +import { HydratedDocument } from 'mongoose'; + +@Schema() +export class Qcm extends AbstractQuestion implements IQcm { + @Prop({ required: true }) + choices: Choice[]; + + type: QuestionType.Qcm; +} + +export type QcmDocument = HydratedDocument<Qcm>; + +export const qcmSchema = SchemaFactory.createForClass(Qcm); diff --git a/server/app/game/schemas/qrl.schema.ts b/server/app/game/schemas/qrl.schema.ts new file mode 100644 index 00000000..33420505 --- /dev/null +++ b/server/app/game/schemas/qrl.schema.ts @@ -0,0 +1,13 @@ +import { Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Qrl as IQrl, QuestionType } from '@common/question'; +import { AbstractQuestion } from './abstract-question.schema'; +import { HydratedDocument } from 'mongoose'; + +@Schema() +export class Qrl extends AbstractQuestion implements IQrl { + type: QuestionType.Qrl; +} + +export type QrlDocument = HydratedDocument<Qrl>; + +export const qrlSchema = SchemaFactory.createForClass(Qrl); diff --git a/server/app/model/dto/qcm/create-qcm.dto.ts b/server/app/model/dto/qcm/create-qcm.dto.ts deleted file mode 100644 index 6282adfa..00000000 --- a/server/app/model/dto/qcm/create-qcm.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Choices } from '@app/model/database/choices'; -import { QCM_MAX_CHOICES, QCM_MAX_POINTS, QCM_MIN_CHOICES, QCM_MIN_POINTS } from '@app/model/dto/qcm/qcm.dto.constants'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ArrayMaxSize, ArrayMinSize, IsArray, IsInt, IsString, Max, Min, ValidateNested } from 'class-validator'; - -export class CreateQcmDto { - @ApiProperty() - @IsString() - question: string; - - @ApiProperty() - @IsInt() - @Min(QCM_MIN_POINTS) - @Max(QCM_MAX_POINTS) - points: number; - - @ApiProperty({ type: () => Choices, isArray: true }) - @IsArray() - @ArrayMinSize(QCM_MIN_CHOICES) - @ArrayMaxSize(QCM_MAX_CHOICES) - @ValidateNested({ each: true }) - @Type(() => Choices) - choices: Choices[]; - - @ApiProperty() - @IsString() - lastModified?: string; - -} diff --git a/server/app/model/dto/qcm/qcm.dto.constants.ts b/server/app/model/dto/qcm/qcm.dto.constants.ts deleted file mode 100644 index e9922869..00000000 --- a/server/app/model/dto/qcm/qcm.dto.constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const QCM_MAX_POINTS = 100; -export const QCM_MIN_POINTS = 10; -export const QCM_MAX_CHOICES = 4; -export const QCM_MIN_CHOICES = 2; -export const QCM_POINT_DIV_FACTOR = 10; -export const QCM_MIN_CORRECT_ANSWER = 1; -export const QCM_MIN_WRONG_ANSWER = 1; \ No newline at end of file diff --git a/server/app/model/dto/qcm/update-qcm.dto.ts b/server/app/model/dto/qcm/update-qcm.dto.ts deleted file mode 100644 index eec9a56a..00000000 --- a/server/app/model/dto/qcm/update-qcm.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Choices } from '@app/model/database/choices'; -import { QCM_MAX_CHOICES, QCM_MAX_POINTS, QCM_MIN_CHOICES, QCM_MIN_POINTS } from '@app/model/dto/qcm/qcm.dto.constants'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ArrayMaxSize, ArrayMinSize, IsArray, IsInt, IsNotEmpty, IsOptional, IsString, Max, Min, ValidateNested } from 'class-validator'; - -export class UpdateQcmDto { - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - question?: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsInt() - @Min(QCM_MIN_POINTS) - @Max(QCM_MAX_POINTS) - points?: number; - - @ApiProperty({ type: () => Choices, isArray: true, required: false }) - @IsOptional() - @IsArray() - @ArrayMinSize(QCM_MIN_CHOICES) - @ArrayMaxSize(QCM_MAX_CHOICES) - @ValidateNested({ each: true }) - @Type(() => Choices) - choices?: Choices[]; - - @ApiProperty() - @IsNotEmpty() - @IsString() - _id?: string; - -} diff --git a/server/app/model/dto/qrl/create-qrl.dto.ts b/server/app/model/dto/qrl/create-qrl.dto.ts deleted file mode 100644 index ae00a276..00000000 --- a/server/app/model/dto/qrl/create-qrl.dto.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { QRL_MAX_POINTS, QRL_MIN_POINTS } from '@app/model/dto/qrl/qrl.dto.constants'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsInt, IsString, Max, Min } from 'class-validator'; - -export class CreateQrlDto { - @ApiProperty() - @IsString() - question: string; - - @ApiProperty() - @IsInt() - @Min(QRL_MIN_POINTS) - @Max(QRL_MAX_POINTS) - points: number; - - @ApiProperty() - @IsString() - lastModified: string; - -} diff --git a/server/app/model/dto/qrl/qrl.dto.constants.ts b/server/app/model/dto/qrl/qrl.dto.constants.ts deleted file mode 100644 index 5a240dc0..00000000 --- a/server/app/model/dto/qrl/qrl.dto.constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const QRL_MAX_POINTS = 100; -export const QRL_MIN_POINTS = 10; -export const QRL_POINT_DIV_FACTOR = 10; diff --git a/server/app/model/dto/qrl/update-qrl.dto.ts b/server/app/model/dto/qrl/update-qrl.dto.ts deleted file mode 100644 index a0ba07e3..00000000 --- a/server/app/model/dto/qrl/update-qrl.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { QRL_MAX_POINTS, QRL_MIN_POINTS } from '@app/model/dto/qrl/qrl.dto.constants'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsInt, IsNotEmpty, IsOptional, IsString, Max, Min } from 'class-validator'; - -export class UpdateQrlDto { - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - question?: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsInt() - @Min(QRL_MIN_POINTS) - @Max(QRL_MAX_POINTS) - points?: number; - - @ApiProperty() - @IsString() - lastModified: string; - - @ApiProperty() - @IsNotEmpty() - @IsString() - _id?: string; -} diff --git a/server/app/services/qcm/qcm.service.spec.ts b/server/app/services/qcm/qcm.service.spec.ts deleted file mode 100644 index 3ceb16e5..00000000 --- a/server/app/services/qcm/qcm.service.spec.ts +++ /dev/null @@ -1,304 +0,0 @@ -import { DateService } from '@app/services/date/date.service'; -import { Logger } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryServer } from 'mongodb-memory-server'; -import { Connection, Model } from 'mongoose'; -import { QcmService } from './qcm.service'; - -import { Qcm, QcmDocument, qcmSchema } from '@app/model/database/qcm'; -import { MongooseModule, getConnectionToken, getModelToken } from '@nestjs/mongoose'; - -describe('QcmService', () => { - let service: QcmService; - let qcmModel: Model<QcmDocument>; - - beforeEach(async () => { - - qcmModel = { - countDocuments: jest.fn(), - insertMany: jest.fn(), - create: jest.fn(), - find: jest.fn(), - findOne: jest.fn(), - deleteOne: jest.fn(), - update: jest.fn(), - updateOne: jest.fn(), - } as unknown as Model<QcmDocument>; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - QcmService, - Logger, - DateService, - { - provide: getModelToken(Qcm.name), - useValue: qcmModel, - }, - ], - }).compile(); - - service = module.get<QcmService>(QcmService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - it('database should be populated when there is no data', async () => { - jest.spyOn(qcmModel, 'countDocuments').mockResolvedValue(0); - const spyPopulateDB = jest.spyOn(service, 'populateDB'); - await service.start(); - expect(spyPopulateDB).toHaveBeenCalled(); - }); - - it('database should not be populated when there is some data', async () => { - jest.spyOn(qcmModel, 'countDocuments').mockResolvedValue(1); - const spyPopulateDB = jest.spyOn(service, 'populateDB'); - await service.start(); - expect(spyPopulateDB).not.toHaveBeenCalled(); - }); -}); - -const DELAY_BEFORE_CLOSING_CONNECTION = 200; - -describe('QcmServiceEndToEnd', () => { - let service: QcmService; - let qcmModel: Model<QcmDocument>; - let mongoServer: MongoMemoryServer; - let connection: Connection; - - beforeEach(async () => { - mongoServer = await MongoMemoryServer.create(); - const module = await Test.createTestingModule({ - imports: [ - MongooseModule.forRootAsync({ - useFactory: () => ({ - uri: mongoServer.getUri(), - }), - }), - MongooseModule.forFeature([{ name: Qcm.name, schema: qcmSchema }]), - ], - providers: [QcmService, Logger, DateService], - }).compile(); - - service = module.get<QcmService>(QcmService); - qcmModel = module.get<Model<QcmDocument>>(getModelToken(Qcm.name)); - connection = await module.get(getConnectionToken()); - }); - - afterEach((done) => { - setTimeout(async () => { - await connection.close(); - await mongoServer.stop(); - done(); - }, DELAY_BEFORE_CLOSING_CONNECTION); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - expect(qcmModel).toBeDefined(); - }); - - it('start() should populate the database when there is no data', async () => { - const spyPopulateDB = jest.spyOn(service, 'populateDB'); - await qcmModel.deleteMany({}); - await service.start(); - expect(spyPopulateDB).toHaveBeenCalled(); - }); - - it('start() should not populate the DB when there is some data', async () => { - const qcm = getFakeQcm(); - await qcmModel.create(qcm); - const spyPopulateDB = jest.spyOn(service, 'populateDB'); - expect(spyPopulateDB).not.toHaveBeenCalled(); - }); - - it('populateDB() should add 2 new qcms', async () => { - const eltCountsBefore = await qcmModel.countDocuments(); - await service.populateDB(); - const eltCountsAfter = await qcmModel.countDocuments(); - expect(eltCountsAfter - eltCountsBefore).toEqual(2); - }); - - it('getAllQcms() return all qcms in database', async () => { - await qcmModel.deleteMany({}); - expect((await service.getAllQcm()).length).toEqual(0); - const qcm = getFakeQcm(); - await qcmModel.create(qcm); - expect((await service.getAllQcm()).length).toEqual(1); - }); - - it('getQcmByQuestion() return qcm with the specified question', async () => { - const qcm = getFakeQcm(); - await qcmModel.create(qcm); - expect(await service.getQcmByQuestion(qcm.question)).toEqual(expect.objectContaining(qcm)); - }); - - it('getQcmQuestion() should return qcm question', async () => { - const qcm = getFakeQcm(); - await qcmModel.create(qcm); - const question = await service.getQcmQuestion(qcm.question); - expect(question).toEqual(qcm.question); - }); - - it('getQcmQuestion() should fail if qcm does not exist', async () => { - const qcm = getFakeQcm(); - await expect(service.getQcmQuestion(qcm.question)).rejects.toBeTruthy(); - }); - - it('getQcmChoices() should return qcm question', async () => { - const qcm = getFakeQcm(); - await qcmModel.create(qcm); - const choices = await service.getQcmChoices(qcm.question); - expect(choices).toEqual(qcm.choices); - }); - - it('getQcmChoices() should fail if qcm does not exist', async () => { - const qcm = getFakeQcm(); - await expect(service.getQcmChoices(qcm.question)).rejects.toBeTruthy(); - }); - - it('modifyQcm() should fail if a qcm with same question exists', async () => { - const qcm = getFakeQcm(); - const qcm2 = getFakeQcm2(); - await qcmModel.create(qcm); - await qcmModel.create(qcm2); - qcm.question = qcm2.question; - await expect(service.modifyQcm(qcm)).rejects.toBeTruthy(); - }); - - it('modifyQcm() should fail if qcm does not exist', async () => { - const qcm = getFakeQcm(); - await expect(service.modifyQcm(qcm)).rejects.toBeTruthy(); - }); - - it('modifyQcm() should fail if mongo query failed', async () => { - jest.spyOn(qcmModel, 'updateOne').mockRejectedValue(''); - const qcm = getFakeQcm(); - await expect(service.modifyQcm(qcm)).rejects.toBeTruthy(); - }); - - it('deleteQcm() should delete the qcm', async () => { - await qcmModel.deleteMany({}); - const qcm = getFakeQcm(); - await qcmModel.create(qcm); - await service.deleteQcm(qcm.question); - expect(await qcmModel.countDocuments()).toEqual(0); - }); - - it('deleteQcm() should fail if the qcm does not exist', async () => { - await qcmModel.deleteMany({}); - const qcm = getFakeQcm(); - await expect(service.deleteQcm(qcm.question)).rejects.toBeTruthy(); - }); - - it('deleteQcm() should fail if mongo query failed', async () => { - jest.spyOn(qcmModel, 'deleteOne').mockRejectedValue(''); - const qcm = getFakeQcm(); - await expect(service.deleteQcm(qcm.question)).rejects.toBeTruthy(); - }); - - it('addQcm() should add the qcm to the DB', async () => { - await qcmModel.deleteMany({}); - const qcm = getFakeQcm(); - await service.addQcm({ ...qcm, question: 'Question1', points: 50 }); - expect(await qcmModel.countDocuments()).toEqual(1); - }); - - it('addQcm() should fail if question already is in DB', async () => { - await qcmModel.deleteMany({}); - const qcm = getFakeQcm(); - await qcmModel.create(qcm); - await expect(service.addQcm(qcm)).rejects.toBeTruthy(); - expect(await qcmModel.countDocuments()).toEqual(1); - }); - - it('addQcm() should fail if mongo query failed', async () => { - jest.spyOn(qcmModel, 'create').mockImplementation(async () => Promise.reject('')); - const qcm = getFakeQcm(); - await expect(service.addQcm({ ...qcm, question: 'Question1', points: 50 })).rejects.toBeTruthy(); - }); - - it('addQcm() should fail if the qcm is not a valid', async () => { - await qcmModel.deleteMany({}); - const qcm = getFakeQcm(); - await expect( - service.addQcm({ - ...qcm, - question: 'Question1', - points: 35, - choices: [ - { choice: 'Oracles', correct: true }, - { choice: 'Oracle', correct: false }, - ] - }) - ).rejects.toBeTruthy(); - await expect( - service.addQcm({ - ...qcm, - question: 'Question1', - points: 0, - choices: [ - { choice: 'Oracles', correct: true }, - { choice: 'Oracle', correct: false }, - ] - }) - ).rejects.toBeTruthy(); - await expect( - service.addQcm({ - ...qcm, - question: 'Question1', - points: 110, - choices: [ - { choice: 'Oracles', correct: true }, - { choice: 'Oracle', correct: false }, - ] - }) - ).rejects.toBeTruthy(); - await expect( - service.addQcm({ - ...qcm, - question: 'Question1', - points: 50, - choices: [ - { choice: 'Oracles', correct: true }, - { choice: 'Oracle', correct: true }, - ] - }) - ).rejects.toBeTruthy(); - await expect( - service.addQcm({ - ...qcm, - question: 'Question1', - points: 50, - choices: [ - { choice: 'Oracles', correct: true }, - { choice: 'Oracle', correct: false }, - { choice: 'Oracle', correct: false }, - ] - }) - ).rejects.toBeTruthy(); - }); -}); - -const getFakeQcm = (): Qcm => ({ - question: 'Quel est le nom de ce jeu?', - points: 100, - choices: [ - { choice: 'Oracles', correct: true }, - { choice: 'Oracle', correct: false }, - ], - lastModified: getRandomString(), -}); -const getFakeQcm2 = (): Qcm => ({ - question: 'Quel est le nom de ce jeu2?', - points: 100, - choices: [ - { choice: 'Oracles', correct: true }, - { choice: 'Oracle', correct: false }, - ], - lastModified: getRandomString(), -}); - -const BASE_36 = 36; -const getRandomString = (): string => (Math.random() + 1).toString(BASE_36).substring(2); diff --git a/server/app/services/qcm/qcm.service.ts b/server/app/services/qcm/qcm.service.ts deleted file mode 100644 index e0ce3f43..00000000 --- a/server/app/services/qcm/qcm.service.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; - -import { Choices } from '@app/model/database/choices'; -import { Qcm, QcmDocument } from '@app/model/database/qcm'; -import { CreateQcmDto } from '@app/model/dto/qcm/create-qcm.dto'; -import { - QCM_MAX_POINTS, - QCM_MIN_CHOICES, - QCM_MIN_CORRECT_ANSWER, - QCM_MIN_POINTS, - QCM_MIN_WRONG_ANSWER, - QCM_POINT_DIV_FACTOR -} from '@app/model/dto/qcm/qcm.dto.constants'; -import { UpdateQcmDto } from '@app/model/dto/qcm/update-qcm.dto'; -import { DateService } from '@app/services/date/date.service'; - -@Injectable() -export class QcmService { - constructor( - @InjectModel(Qcm.name) public qcmModel: Model<QcmDocument>, - private readonly logger: Logger, - private readonly dateService: DateService, - ) { - this.start(); - } - - async start() { - if ((await this.qcmModel.countDocuments()) === 0) { - await this.populateDB(); - } - } - - async populateDB(): Promise<void> { - const qcmQuestions: CreateQcmDto[] = [ - { - question: 'Dans quelle ville est poly?', - points: 10, - choices: [ - { choice: 'Montreal', correct: true }, - { choice: 'Quebec', correct: false }, - { choice: 'Sherbrooke', correct: false }, - { choice: 'Toronto', correct: false }, - ], - lastModified: this.dateService.currentTime(), - }, - { - question: 'Combien de membres dans cette équipe de projet 2?', - points: 100, - choices: [ - { choice: '6', correct: true }, - { choice: '5', correct: false }, - ], - lastModified: this.dateService.currentTime(), - }, - ]; - - this.logger.log('THIS ADDS DATA TO THE DATABASE, DO NOT USE OTHERWISE'); - await this.qcmModel.insertMany(qcmQuestions); - } - - async getAllQcm(): Promise<Qcm[]> { - return await this.qcmModel.find({}); - } - - async getQcmByQuestion(question: string): Promise<Qcm> { - return await this.qcmModel.findOne({ question: question }); - } - - async addQcm(qcm: CreateQcmDto): Promise<void> { - if (!this.validateQcm(qcm)) { - return Promise.reject('Invalid qcm'); - } - const existingQcm = await this.getQcmByQuestion(qcm.question); - if (existingQcm) { - return Promise.reject('Qcm already exists'); - } - const currentDate = this.dateService.currentTime(); - const newQcm = { ...qcm, lastModified: currentDate }; - try { - await this.qcmModel.create(newQcm); - } catch (error) { - return Promise.reject(`Failed to insert course: ${error}`); - } - } - - async deleteQcm(question: string): Promise<void> { - try { - const res = await this.qcmModel.deleteOne({ question: question }); - if (res.deletedCount === 0) { - return Promise.reject('Could not find course'); - } - } catch (error) { - return Promise.reject(`Failed to delete course: ${error}`); - } - } - - async modifyQcm(qcm: UpdateQcmDto): Promise<void> { - const currentDate = this.dateService.currentTime(); - const filterQuery = { id: qcm._id }; - const updatedQcm = { ...qcm, lastModified: currentDate }; - try { - const qcmFound = await this.getQcmByQuestion(qcm.question); - if (!qcmFound) { - return Promise.reject('Qcm does not exist'); - } - if (qcmFound && qcm._id !== qcmFound._id) { - return Promise.reject('Another qcm already has the same question'); - } - const res = await this.qcmModel.replaceOne(filterQuery, updatedQcm); - if (res.matchedCount === 0) { - return Promise.reject('Could not find qcm'); - } - } catch (error) { - return Promise.reject(`Failed to update qcm: ${error}`); - } - } - - async getQcmQuestion(question: string): Promise<string> { - const filterQuery = { question: question }; - try { - const res = await this.qcmModel.findOne(filterQuery, { question: 1 }); - return res.question; - } catch (error) { - return Promise.reject(`Failed to get data: ${error}`); - } - } - async getQcmChoices(question: string): Promise<Choices[]> { - const filterQuery = { question: question }; - try { - const res = await this.qcmModel.findOne(filterQuery, { choices: 1 }); - return res.choices; - } catch (error) { - return Promise.reject(`Failed to get data: ${error}`); - } - } - - private validateQcm(qcm: CreateQcmDto): boolean { - return this.validatePoints(qcm.points) && this.validateChoices(qcm.choices); - } - private validatePoints(points: number): boolean { - return points % QCM_POINT_DIV_FACTOR === 0 && points >= QCM_MIN_POINTS && points <= QCM_MAX_POINTS; - } - private validateChoices(choices: Choices[]): boolean { - let correctChoices = 0; - let incorrectChoices = 0; - choices.forEach((choice) => { - if (choice.correct) { - correctChoices++; - } else { - incorrectChoices++; - } - }); - const sumChoices = correctChoices + incorrectChoices; - return ( - correctChoices >= QCM_MIN_CORRECT_ANSWER && - incorrectChoices >= QCM_MIN_WRONG_ANSWER && - (sumChoices === QCM_MIN_CHOICES || sumChoices === QCM_MAX_POINTS) - ); - } -} diff --git a/server/app/services/qrl/qrl.service.spec.ts b/server/app/services/qrl/qrl.service.spec.ts deleted file mode 100644 index 78ce7edd..00000000 --- a/server/app/services/qrl/qrl.service.spec.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { DateService } from '@app/services/date/date.service'; -import { Logger } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { MongoMemoryServer } from 'mongodb-memory-server'; -import { Connection, Model } from 'mongoose'; -import { QrlService } from './qrl.service'; - -import { Qrl, QrlDocument, qrlSchema } from '@app/model/database/qrl'; -import { MongooseModule, getConnectionToken, getModelToken } from '@nestjs/mongoose'; - -describe('QrlService', () => { - let service: QrlService; - let qrlModel: Model<QrlDocument>; - - beforeEach(async () => { - - qrlModel = { - countDocuments: jest.fn(), - insertMany: jest.fn(), - create: jest.fn(), - find: jest.fn(), - findOne: jest.fn(), - deleteOne: jest.fn(), - update: jest.fn(), - updateOne: jest.fn(), - } as unknown as Model<QrlDocument>; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - QrlService, - Logger, - DateService, - { - provide: getModelToken(Qrl.name), - useValue: qrlModel, - }, - ], - }).compile(); - - service = module.get<QrlService>(QrlService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - it('database should be populated when there is no data', async () => { - jest.spyOn(qrlModel, 'countDocuments').mockResolvedValue(0); - const spyPopulateDB = jest.spyOn(service, 'populateDB'); - await service.start(); - expect(spyPopulateDB).toHaveBeenCalled(); - }); - - it('database should not be populated when there is some data', async () => { - jest.spyOn(qrlModel, 'countDocuments').mockResolvedValue(1); - const spyPopulateDB = jest.spyOn(service, 'populateDB'); - await service.start(); - expect(spyPopulateDB).not.toHaveBeenCalled(); - }); -}); - -const DELAY_BEFORE_CLOSING_CONNECTION = 200; - -describe('QrlServiceEndToEnd', () => { - let service: QrlService; - let qrlModel: Model<QrlDocument>; - let mongoServer: MongoMemoryServer; - let connection: Connection; - - beforeEach(async () => { - mongoServer = await MongoMemoryServer.create(); - const module = await Test.createTestingModule({ - imports: [ - MongooseModule.forRootAsync({ - useFactory: () => ({ - uri: mongoServer.getUri(), - }), - }), - MongooseModule.forFeature([{ name: Qrl.name, schema: qrlSchema }]), - ], - providers: [QrlService, Logger, DateService], - }).compile(); - - service = module.get<QrlService>(QrlService); - qrlModel = module.get<Model<QrlDocument>>(getModelToken(Qrl.name)); - connection = await module.get(getConnectionToken()); - }); - - afterEach((done) => { - setTimeout(async () => { - await connection.close(); - await mongoServer.stop(); - done(); - }, DELAY_BEFORE_CLOSING_CONNECTION); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - expect(qrlModel).toBeDefined(); - }); - - it('start() should populate the database when there is no data', async () => { - const spyPopulateDB = jest.spyOn(service, 'populateDB'); - await qrlModel.deleteMany({}); - await service.start(); - expect(spyPopulateDB).toHaveBeenCalled(); - }); - - it('start() should not populate the DB when there is some data', async () => { - const qrl = getFakeQrl(); - await qrlModel.create(qrl); - const spyPopulateDB = jest.spyOn(service, 'populateDB'); - expect(spyPopulateDB).not.toHaveBeenCalled(); - }); - - it('populateDB() should add 2 new qrls', async () => { - const eltCountsBefore = await qrlModel.countDocuments(); - await service.populateDB(); - const eltCountsAfter = await qrlModel.countDocuments(); - expect(eltCountsAfter - eltCountsBefore).toEqual(2); - }); - - it('getAllQrl() return all qrls in database', async () => { - await qrlModel.deleteMany({}); - expect((await service.getAllQrl()).length).toEqual(0); - const qrl = getFakeQrl(); - await qrlModel.create(qrl); - expect((await service.getAllQrl()).length).toEqual(1); - }); - - it('getQrlByQuestion() return qrl with the specified question', async () => { - const qrl = getFakeQrl(); - await qrlModel.create(qrl); - expect(await service.getQrlByQuestion(qrl.question)).toEqual(expect.objectContaining(qrl)); - }); - - it('getQrlQuestion() should return qrl question', async () => { - const qrl = getFakeQrl(); - await qrlModel.create(qrl); - const question = await service.getQrlQuestion(qrl.question); - expect(question).toEqual(qrl.question); - }); - - it('getQrlQuestion() should fail if qrl does not exist', async () => { - const qrl = getFakeQrl(); - await expect(service.getQrlQuestion(qrl.question)).rejects.toBeTruthy(); - }); - - it('modifyQrl() should fail if a qrl with same question exists', async () => { - const qrl = getFakeQrl(); - const qrl2 = getFakeQrl2(); - await qrlModel.create(qrl); - await qrlModel.create(qrl2); - qrl.question = qrl2.question; - await expect(service.modifyQrl(qrl)).rejects.toBeTruthy(); - }); - - it('modifyQrl() should fail if qrl does not exist', async () => { - const qrl = getFakeQrl(); - await expect(service.modifyQrl(qrl)).rejects.toBeTruthy(); - }); - - it('modifyQrl() should fail if mongo query failed', async () => { - jest.spyOn(qrlModel, 'updateOne').mockRejectedValue(''); - const qrl = getFakeQrl(); - await expect(service.modifyQrl(qrl)).rejects.toBeTruthy(); - }); - - it('deleteQrl() should delete the qrl', async () => { - await qrlModel.deleteMany({}); - const qrl = getFakeQrl(); - await qrlModel.create(qrl); - await service.deleteQrl(qrl.question); - expect(await qrlModel.countDocuments()).toEqual(0); - }); - - it('deleteQrl() should fail if the qrl does not exist', async () => { - await qrlModel.deleteMany({}); - const qrl = getFakeQrl(); - await expect(service.deleteQrl(qrl.question)).rejects.toBeTruthy(); - }); - - it('deleteQrl() should fail if mongo query failed', async () => { - jest.spyOn(qrlModel, 'deleteOne').mockRejectedValue(''); - const qrl = getFakeQrl(); - await expect(service.deleteQrl(qrl.question)).rejects.toBeTruthy(); - }); - - it('addQrl() should add the qrl to the DB', async () => { - await qrlModel.deleteMany({}); - const qrl = getFakeQrl(); - await service.addQrl(qrl); - expect(await qrlModel.countDocuments()).toEqual(1); - }); - - it('addQrl() should fail if question already is in DB', async () => { - await qrlModel.deleteMany({}); - const qrl = getFakeQrl(); - await qrlModel.create(qrl); - await expect(service.addQrl(qrl)).rejects.toBeTruthy(); - expect(await qrlModel.countDocuments()).toEqual(1); - }); - - it('addQrl() should fail if mongo query failed', async () => { - jest.spyOn(qrlModel, 'create').mockImplementation(async () => Promise.reject('')); - const qrl = getFakeQrl(); - await expect(service.addQrl({ ...qrl, question: 'Question1', points: 50 })).rejects.toBeTruthy(); - }); - - it('addQrl() should fail if the qrl is not a valid', async () => { - const qrl = getFakeQrl(); - await expect(service.addQrl({ ...qrl, question: 'Question1', points: 35 })).rejects.toBeTruthy(); - await expect(service.addQrl({ ...qrl, question: 'Question1', points: 0 })).rejects.toBeTruthy(); - await expect(service.addQrl({ ...qrl, question: 'Question1', points: 110 })).rejects.toBeTruthy(); - }); -}); - -const getFakeQrl = (): Qrl => ({ - question: 'Quel est le nom de ce jeu?', - points: 100, - lastModified: getRandomString(), -}); -const getFakeQrl2 = (): Qrl => ({ - question: 'Quel est le nom de ce jeu2?', - points: 100, - lastModified: getRandomString(), -}); - -const BASE_36 = 36; -const getRandomString = (): string => (Math.random() + 1).toString(BASE_36).substring(2); diff --git a/server/app/services/qrl/qrl.service.ts b/server/app/services/qrl/qrl.service.ts deleted file mode 100644 index 7fcdb99a..00000000 --- a/server/app/services/qrl/qrl.service.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; - -import { Qrl, QrlDocument } from '@app/model/database/qrl'; -import { CreateQrlDto } from '@app/model/dto/qrl/create-qrl.dto'; -import { QRL_MAX_POINTS, QRL_MIN_POINTS, QRL_POINT_DIV_FACTOR } from '@app/model/dto/qrl/qrl.dto.constants'; -import { UpdateQrlDto } from '@app/model/dto/qrl/update-qrl.dto'; -import { DateService } from '@app/services/date/date.service'; - -@Injectable() -export class QrlService { - constructor( - @InjectModel(Qrl.name) public qrlModel: Model<QrlDocument>, - private readonly logger: Logger, - private readonly dateService: DateService, - ) { - this.start(); - } - - async start() { - if ((await this.qrlModel.countDocuments()) === 0) { - await this.populateDB(); - } - } - - async populateDB(): Promise<void> { - const qrlQuestions: CreateQrlDto[] = [ - { - question: 'Dans quelle ville est poly?', - points: 10, - lastModified: this.dateService.currentTime(), - }, - { - question: 'Combien de membres dans cette équipe de projet 2?', - points: 100, - lastModified: this.dateService.currentTime(), - }, - ]; - - this.logger.log('THIS ADDS DATA TO THE DATABASE, DO NOT USE OTHERWISE'); - await this.qrlModel.insertMany(qrlQuestions); - } - - async getAllQrl(): Promise<Qrl[]> { - return await this.qrlModel.find({}); - } - - async getQrlById(id: string): Promise<Qrl> { - return await this.qrlModel.findById(id); - } - async getQrlByQuestion(question: string): Promise<Qrl> { - return await this.qrlModel.findOne({ question: question }); - } - - async addQrl(qrl: CreateQrlDto): Promise<void> { - if (!this.validateQrl(qrl)) { - return Promise.reject('Invalid qrl'); - } - const existingQrl = await this.getQrlByQuestion(qrl.question); - if (existingQrl) { - return Promise.reject('Qrl already exists'); - } - const currentDate = this.dateService.currentTime(); - const newQrl = { ...qrl, lastModified: currentDate }; - try { - await this.qrlModel.create(newQrl); - } catch (error) { - return Promise.reject(`Failed to insert course: ${error}`); - } - } - - async deleteQrl(question: string): Promise<void> { - try { - const res = await this.qrlModel.deleteOne({ question: question }); - if (res.deletedCount === 0) { - return Promise.reject('Could not find course'); - } - } catch (error) { - return Promise.reject(`Failed to delete course: ${error}`); - } - } - - async modifyQrl(qrl: UpdateQrlDto): Promise<void> { - const currentDate = this.dateService.currentTime(); - const filterQuery = { question: qrl.question }; - const updatedQrl = { ...qrl, lastModified: currentDate }; - try { - const qrlFound = await this.getQrlByQuestion(qrl.question); - if (!qrlFound) { - return Promise.reject('Qrl does not exist'); - } - if (qrlFound && qrl._id !== qrlFound._id) { - return Promise.reject('Another qrl already has the same question'); - } - const res = await this.qrlModel.replaceOne(filterQuery, updatedQrl); - if (res.matchedCount === 0) { - return Promise.reject('Could not find qrl'); - } - } catch (error) { - return Promise.reject(`Failed to update qrl: ${error}`); - } - } - - async getQrlQuestion(question: string): Promise<string> { - const filterQuery = { question: question }; - try { - const res = await this.qrlModel.findOne(filterQuery, { question: 1 }); - return res.question; - } catch (error) { - return Promise.reject(`Failed to get data: ${error}`); - } - } - - private validateQrl(qrl: CreateQrlDto): boolean { - return this.validatePoints(qrl.points); - } - private validatePoints(points: number): boolean { - return points % QRL_POINT_DIV_FACTOR === 0 && points >= QRL_MIN_POINTS && points <= QRL_MAX_POINTS; - } -} -- GitLab From 13bd8898b0b57b5fa50b1d5ccf14946985298a31 Mon Sep 17 00:00:00 2001 From: Laurent Bourgon <laurent.bourgon@polymtl.ca> Date: Thu, 25 Jan 2024 15:13:27 -0500 Subject: [PATCH 7/7] remove old schemas --- server/app/model/database/choices.ts | 4 ---- server/app/model/database/qcm.ts | 30 ---------------------------- server/app/model/database/qrl.ts | 25 ----------------------- 3 files changed, 59 deletions(-) delete mode 100644 server/app/model/database/choices.ts delete mode 100644 server/app/model/database/qcm.ts delete mode 100644 server/app/model/database/qrl.ts diff --git a/server/app/model/database/choices.ts b/server/app/model/database/choices.ts deleted file mode 100644 index d5e2bf3e..00000000 --- a/server/app/model/database/choices.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class Choices { - choice: string; - correct: boolean; -} \ No newline at end of file diff --git a/server/app/model/database/qcm.ts b/server/app/model/database/qcm.ts deleted file mode 100644 index 1c57fc55..00000000 --- a/server/app/model/database/qcm.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { ApiProperty } from '@nestjs/swagger'; -import { Document } from 'mongoose'; -import { Choices } from './choices'; - -export type QcmDocument = Qcm & Document; - -@Schema() -export class Qcm { - @ApiProperty() - @Prop({ required: true }) - question: string; - - @ApiProperty() - @Prop({ required: true }) - points: number; - - @ApiProperty() - @Prop({ required: true }) - choices: Choices[]; - - @ApiProperty() - @Prop({ required: true }) - lastModified: string; - - @ApiProperty() - _id?: string; -} - -export const qcmSchema = SchemaFactory.createForClass(Qcm); diff --git a/server/app/model/database/qrl.ts b/server/app/model/database/qrl.ts deleted file mode 100644 index da2d7c2e..00000000 --- a/server/app/model/database/qrl.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { ApiProperty } from '@nestjs/swagger'; -import { Document } from 'mongoose'; - -export type QrlDocument = Qrl & Document; - -@Schema() -export class Qrl { - @ApiProperty() - @Prop({ required: true }) - question: string; - - @ApiProperty() - @Prop({ required: true }) - points: number; - - @ApiProperty() - @Prop({ required: true }) - lastModified: string; - - @ApiProperty() - _id?: string; -} - -export const qrlSchema = SchemaFactory.createForClass(Qrl); -- GitLab