diff --git a/src/modules/admin/questions/admin-questions.controller.ts b/src/modules/admin/questions/admin-questions.controller.ts index 2ec39c9..524d707 100644 --- a/src/modules/admin/questions/admin-questions.controller.ts +++ b/src/modules/admin/questions/admin-questions.controller.ts @@ -89,7 +89,7 @@ export class AdminQuestionsController { @Get(':id') public async getQuestionById(@Param('id', ParseIntPipe) id: number) { - return await this.questionsService.findOneForAdmin(id); + return await this.questionsService.findOneByIdForAdmin(id); } @Patch(':id') diff --git a/src/modules/questions/dtos/create-question.dto.ts b/src/modules/questions/dtos/create-question.dto.ts index 6286f37..689df78 100644 --- a/src/modules/questions/dtos/create-question.dto.ts +++ b/src/modules/questions/dtos/create-question.dto.ts @@ -38,17 +38,13 @@ export class CreateQuestionDTO { }) content!: string; - @IsPositive({ - message: t('validation.wrongField', { - field: '$t(questions.fields.order)', - }), - }) - @IsNotEmpty({ - message: t('validation.requiredField', { - field: '$t(questions.fields.order)', - }), - }) - order!: number; + // @IsPositive({ + // message: t('validation.wrongField', { + // field: '$t(questions.fields.order)', + // }), + // }) + // @IsOptional() + // order?: number; @IsPositive({ message: t('validation.wrongField', { diff --git a/src/modules/questions/providers/questions-command.provider.ts b/src/modules/questions/providers/questions-command.provider.ts index db74b6c..ca0e68f 100644 --- a/src/modules/questions/providers/questions-command.provider.ts +++ b/src/modules/questions/providers/questions-command.provider.ts @@ -1,7 +1,7 @@ import { ConflictException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Question } from '../entities/question.entity'; -import { QueryFailedError, Repository } from 'typeorm'; +import { DataSource, QueryFailedError, Repository } from 'typeorm'; import { TestsService } from '@/modules/tests/providers/tests.service'; import { I18nService } from 'nestjs-i18n'; import { CreateQuestionDTO } from '../dtos/create-question.dto'; @@ -11,10 +11,16 @@ import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception'; import { UpdateQuestionDTO } from '../dtos/update-question.dto'; import { validateMetadata } from '../helpers/validate-metadata.helper'; import { AppException } from '@/common/exceptions/app.exception'; +import { Test } from '@/modules/tests/entities/test.entity'; @Injectable() export class QuestionsCommandProvider { constructor( + /** + * Inject DataSource + */ + private readonly dataSource: DataSource, + /** * Inject Questions Repository */ @@ -35,13 +41,31 @@ export class QuestionsCommandProvider { public async createOne(createQuestionDto: CreateQuestionDTO) { const test = await this.testsService.adminGetOne(createQuestionDto.test); + let maxOrder: { max: string } | undefined; + + try { + maxOrder = await this.questionsRepository + .createQueryBuilder('q') + .select('MAX(q.order)', 'max') + .where('q.testId = :testId', { testId: test.id }) + .getRawOne(); + } catch (err) { + throw new AppTimeoutException(err); + } + const newQuestion = this.questionsRepository.create({ ...createQuestionDto, test, + order: (Number(maxOrder?.max) || 0) + 1, }); try { - await this.questionsRepository.save(newQuestion); + await this.dataSource.transaction(async (manager) => { + await manager.save(newQuestion); + + await manager.increment(Test, { id: test.id }, 'questionsCount', 1); + }); + return new AppResponse( newQuestion, this.i18nService.translate('common.messages.createSuccess', { @@ -117,7 +141,10 @@ export class QuestionsCommandProvider { let question: Question | null = null; try { - question = await this.questionsRepository.findOneBy({ id }); + question = await this.questionsRepository.findOne({ + where: { id }, + relations: { test: true }, + }); } catch (err) { throw new AppTimeoutException(err); } @@ -130,7 +157,16 @@ export class QuestionsCommandProvider { ); try { - await this.questionsRepository.delete(question.id); + await this.dataSource.transaction(async (manager) => { + await manager.delete(Question, question.id); + + await manager.decrement( + Test, + { id: question.test.id }, + 'questionsCount', + 1, + ); + }); } catch (err) { console.log(err); throw new AppTimeoutException(err); diff --git a/src/modules/questions/providers/questions-query.provider.ts b/src/modules/questions/providers/questions-query.provider.ts index d93b727..a6a9003 100644 --- a/src/modules/questions/providers/questions-query.provider.ts +++ b/src/modules/questions/providers/questions-query.provider.ts @@ -7,7 +7,7 @@ import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception'; import { PaginatedResponse } from '@/common/responses'; import { InjectRepository } from '@nestjs/typeorm'; import { Question } from '../entities/question.entity'; -import { Repository } from 'typeorm'; +import { FindManyOptions, FindOneOptions, Repository } from 'typeorm'; import { TestsService } from '@/modules/tests/providers/tests.service'; import { AppException } from '@/common/exceptions/app.exception'; @@ -39,8 +39,6 @@ export class QuestionsQueryProvider { }); } - console.log(selectedTest); - try { const [questions, total] = await this.questionsRepository.findAndCount({ where: { @@ -69,7 +67,19 @@ export class QuestionsQueryProvider { } } - public async findOneForAdmin(id: number) { + public async findByOptionsForAdmin(options: FindManyOptions) { + let questions: Array = []; + + try { + questions = await this.questionsRepository.find(options); + } catch (err) { + throw new AppTimeoutException(err); + } + + return questions; + } + + public async findOneByIdForAdmin(id: number) { let question: Question | null = null; try { @@ -91,4 +101,16 @@ export class QuestionsQueryProvider { return question; } + + public async findOneForAdmin(options: FindOneOptions) { + let question: Question | null = null; + + try { + question = await this.questionsRepository.findOne(options); + } catch (err) { + throw new AppTimeoutException(err); + } + + return question; + } } diff --git a/src/modules/questions/providers/questions.service.ts b/src/modules/questions/providers/questions.service.ts index e649211..2ec15d0 100644 --- a/src/modules/questions/providers/questions.service.ts +++ b/src/modules/questions/providers/questions.service.ts @@ -4,6 +4,8 @@ import { FindQuestionsDTO } from '../dtos/find-questions.dto'; import { UpdateQuestionDTO } from '../dtos/update-question.dto'; import { QuestionsCommandProvider } from './questions-command.provider'; import { QuestionsQueryProvider } from './questions-query.provider'; +import { FindManyOptions, FindOneOptions } from 'typeorm'; +import { Question } from '../entities/question.entity'; @Injectable() export class QuestionsService { @@ -27,8 +29,16 @@ export class QuestionsService { return await this.questionsQueryProvider.findAllForAdmin(query); } - public async findOneForAdmin(id: number) { - return await this.questionsQueryProvider.findOneForAdmin(id); + public async findByOptionsForAdmin(options: FindManyOptions) { + return await this.questionsQueryProvider.findByOptionsForAdmin(options); + } + + public async findOneByIdForAdmin(id: number) { + return await this.questionsQueryProvider.findOneByIdForAdmin(id); + } + + public async findOneForAdmin(options: FindOneOptions) { + return await this.questionsQueryProvider.findOneForAdmin(options); } public async updateOne(id: number, updateQuestionDto: UpdateQuestionDTO) { diff --git a/src/modules/submissions/entities/submission.entity.ts b/src/modules/submissions/entities/submission.entity.ts index 2ac65ce..8fe701a 100644 --- a/src/modules/submissions/entities/submission.entity.ts +++ b/src/modules/submissions/entities/submission.entity.ts @@ -31,6 +31,9 @@ export class Submission { }) status!: SubmissionStatus; + @Column({ type: 'int', nullable: false, default: 1 }) + currentQuestionOrder!: number; + @OneToMany(() => SubmissionAnswer, (answer) => answer.submission) answers!: Array; diff --git a/src/modules/submissions/providers/answer-submission.provider.ts b/src/modules/submissions/providers/answer-submission.provider.ts index 9780c6f..a78b051 100644 --- a/src/modules/submissions/providers/answer-submission.provider.ts +++ b/src/modules/submissions/providers/answer-submission.provider.ts @@ -1,9 +1,4 @@ -import { - BadRequestException, - ForbiddenException, - HttpStatus, - Injectable, -} from '@nestjs/common'; +import { BadRequestException, HttpStatus, Injectable } from '@nestjs/common'; import { Repository } from 'typeorm'; import { Submission } from '../entities/submission.entity'; import { InjectRepository } from '@nestjs/typeorm'; @@ -21,6 +16,8 @@ import { NumericMetadata } from '@/modules/questions/interfaces/numeric-metadata import { SubmissionAnswer } from '../entities/submission-answer.entity'; import { ChoicesService } from '@/modules/questions/providers/choices.service'; import { CalculateResultProvider } from './calculate-result.provider'; +import { QuestionsService } from '@/modules/questions/providers/questions.service'; +import { SubmissionResult } from '../entities/submission-result.entity'; @Injectable() export class AnswerSubmissionProvider { @@ -42,6 +39,17 @@ export class AnswerSubmissionProvider { @InjectRepository(SubmissionAnswer) private readonly submissionAnswerRepository: Repository, + /** + * Inject SubmissionResult Repository + */ + @InjectRepository(SubmissionResult) + private readonly submissionResultRepository: Repository, + + /** + * Inject Questions Service + */ + private readonly questionsService: QuestionsService, + /** * Inject Choices Service */ @@ -84,35 +92,9 @@ export class AnswerSubmissionProvider { try { submission = await this.submissionRepository.findOne({ - where: { id: submissionId }, + where: { id: submissionId, user: { id: user.id } }, relations: { - user: true, - test: { - questions: { - choices: true, - rangeRules: true, - }, - }, - answers: { - question: true, - }, - result: { - resultRange: true, - }, - }, - order: { - answers: { - question: { - order: 'ASC', - }, - }, - test: { - questions: { - choices: { - order: 'ASC', - }, - }, - }, + test: true, }, }); } catch (err) { @@ -127,44 +109,69 @@ export class AnswerSubmissionProvider { ); } - if (submission.user.id !== user.id) { - throw new ForbiddenException('auth.errors.accessDenied'); - } + // if (submission.user.id !== user.id) { + // throw new ForbiddenException('auth.errors.accessDenied'); + // } - const allQuestions = [...submission.test.questions].sort( - (a, b) => a.order - b.order, - ); - const answeredQuestions = submission.answers.map((el) => el.question); + // const allQuestions = [...submission.test.questions].sort( + // (a, b) => a.order - b.order, + // ); + // const answeredQuestions = submission.answers.map((el) => el.question); - const questionStats = this.getQuestionStats( - allQuestions, - answeredQuestions, - ); + // const questionStats = this.getQuestionStats( + // allQuestions, + // answeredQuestions, + // ); + + const currentQuestion: Question | null = + await this.questionsService.findOneForAdmin({ + where: { + test: { id: submission.test.id }, + order: submission.currentQuestionOrder, + }, + relations: { + choices: true, + rangeRules: true, + }, + }); + + if (!currentQuestion) { + let result: SubmissionResult | null; + + try { + result = await this.submissionResultRepository.findOne({ + where: { submission: { id: submission.id } }, + relations: { + resultRange: true, + }, + }); + } catch (err) { + throw new AppTimeoutException(err); + } - if (!questionStats.current) { return new AppResponse({ isCompleted: true, currentQuestion: null, - result: submission.result + result: result ? { - score: submission.result.score, - probability: submission.result.probability, - title: submission.result.resultRange?.title, - description: submission.result.resultRange?.description, + score: result.score, + probability: result.probability, + title: result.resultRange?.title, + description: result.resultRange?.description, } : null, }); } - this.checkAnswerCorrectness(questionStats.current, answerSubmissionDto); + this.checkAnswerCorrectness(currentQuestion, answerSubmissionDto); const submissionAnswer = this.submissionAnswerRepository.create({ submission, - question: questionStats.current, + question: currentQuestion, }); if ( - questionStats.current.type === QuestionType.MULTI_CHOICE && + currentQuestion.type === QuestionType.MULTI_CHOICE && answerSubmissionDto.choiceId ) { const choice = await this.choicesService.findOneForAdmin( @@ -181,7 +188,7 @@ export class AnswerSubmissionProvider { submissionAnswer.choice = choice; } else if ( - questionStats.current.type === QuestionType.NUMERIC && + currentQuestion.type === QuestionType.NUMERIC && answerSubmissionDto.numericValue ) { submissionAnswer.numericValue = answerSubmissionDto.numericValue; @@ -193,7 +200,19 @@ export class AnswerSubmissionProvider { throw new AppTimeoutException(err); } - if (!questionStats.next) { + const nextQuestion: Question | null = + await this.questionsService.findOneForAdmin({ + where: { + test: { id: submission.test.id }, + order: submission.currentQuestionOrder + 1, + }, + relations: { + choices: true, + rangeRules: true, + }, + }); + + if (!nextQuestion) { const result = await this.calculateResultProvider.calculate(submission); return new AppResponse({ isCompleted: true, @@ -202,14 +221,9 @@ export class AnswerSubmissionProvider { }); } - const nextQuestion = questionStats.next; - return new AppResponse({ isCompleted: false, - isFinal: - nextQuestion.id === allQuestions[allQuestions.length - 1].id - ? true - : undefined, + // isFinal: , currentQuestion: { type: nextQuestion.type, content: nextQuestion.content, diff --git a/src/modules/submissions/providers/calculate-result.provider.ts b/src/modules/submissions/providers/calculate-result.provider.ts index defa3a3..e8f7102 100644 --- a/src/modules/submissions/providers/calculate-result.provider.ts +++ b/src/modules/submissions/providers/calculate-result.provider.ts @@ -7,6 +7,7 @@ import { QuestionType } from '@/modules/questions/enums/question-type.enum'; import { QuestionsService } from '@/modules/questions/providers/questions.service'; import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception'; import { ResultRangeService } from '@/modules/scoring/providers/result-range.service'; +import { SubmissionAnswer } from '../entities/submission-answer.entity'; @Injectable() export class CalculateResultProvider { @@ -26,13 +27,33 @@ export class CalculateResultProvider { * Inject ResultRange Service */ private readonly resultRangeService: ResultRangeService, + + /** + * Inject SubmissionAnswer Repository + */ + @InjectRepository(SubmissionAnswer) + private readonly submissionAnswerRepository: Repository, ) {} public async calculate(submission: Submission) { let score = 0; + let submissionAnswers: Array = []; + + try { + submissionAnswers = await this.submissionAnswerRepository.find({ + where: { submission: { id: submission.id } }, + relations: { + question: true, + choice: true, + }, + }); + } catch (err) { + throw new AppTimeoutException(err); + } + await Promise.all( - submission.answers.map(async (answer) => { + submissionAnswers.map(async (answer) => { if ( answer.question.type === QuestionType.MULTI_CHOICE && answer.choice @@ -42,7 +63,7 @@ export class CalculateResultProvider { answer.question.type === QuestionType.NUMERIC && answer.numericValue !== undefined ) { - const question = await this.questionsService.findOneForAdmin( + const question = await this.questionsService.findOneByIdForAdmin( answer.question.id, ); @@ -63,7 +84,15 @@ export class CalculateResultProvider { let totalScores = 0; - submission.test.questions.forEach((question) => { + const testQuestions = await this.questionsService.findByOptionsForAdmin({ + where: { test: { id: submission.test.id } }, + relations: { + choices: true, + rangeRules: true, + }, + }); + + testQuestions.forEach((question) => { const answerScores: Array = []; if (question.type === QuestionType.MULTI_CHOICE) { question.choices.forEach((choice) => { diff --git a/src/modules/tests/entities/test.entity.ts b/src/modules/tests/entities/test.entity.ts index 8841b95..2efa916 100644 --- a/src/modules/tests/entities/test.entity.ts +++ b/src/modules/tests/entities/test.entity.ts @@ -56,6 +56,9 @@ export class Test { @OneToMany(() => Question, (question) => question.test) questions!: Array; + @Column({ type: 'int', nullable: false, default: 0 }) + questionsCount!: number; + @OneToMany(() => ResultRange, (range) => range.test) resultRanges!: Array; diff --git a/src/modules/tests/providers/tests-command.provider.ts b/src/modules/tests/providers/tests-command.provider.ts index 99db52b..9ad7bed 100644 --- a/src/modules/tests/providers/tests-command.provider.ts +++ b/src/modules/tests/providers/tests-command.provider.ts @@ -105,4 +105,28 @@ export class TestsCommandProvider { throw new AppTimeoutException(err); } } + + public async incrementQuestionsCount(testId: number, count?: number) { + try { + await this.testRepository.increment( + { id: testId }, + 'questionsCount', + count || 1, + ); + } catch (err) { + throw new AppTimeoutException(err); + } + } + + public async decrementQuestionsCount(testId: number, count?: number) { + try { + await this.testRepository.decrement( + { id: testId }, + 'questionsCount', + count || 1, + ); + } catch (err) { + throw new AppTimeoutException(err); + } + } } diff --git a/src/modules/tests/providers/tests.service.ts b/src/modules/tests/providers/tests.service.ts index 942719f..7baa30f 100644 --- a/src/modules/tests/providers/tests.service.ts +++ b/src/modules/tests/providers/tests.service.ts @@ -48,4 +48,18 @@ export class TestsService { public async deleteOne(id: number) { return await this.testsCommandProvider.deleteOne(id); } + + public async incrementQuestionsCount(testId: number, count?: number) { + return await this.testsCommandProvider.incrementQuestionsCount( + testId, + count, + ); + } + + public async decrementQuestionsCount(testId: number, count?: number) { + return await this.testsCommandProvider.decrementQuestionsCount( + testId, + count, + ); + } }