feat: add transactions
This commit is contained in:
@@ -3,14 +3,14 @@ Authorization: Bearer {{$global.accessToken}}
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"testId": 4
|
"testId": 1
|
||||||
}
|
}
|
||||||
|
|
||||||
POST {{baseUrl}}/submissions/10/answer
|
POST {{baseUrl}}/submissions/2/answer
|
||||||
Authorization: Bearer {{$global.accessToken}}
|
Authorization: Bearer {{$global.accessToken}}
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
// "choiceId": 1
|
// "choiceId": 2
|
||||||
"numericValue": 60
|
"numericValue": 8
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BadRequestException, HttpStatus, Injectable } from '@nestjs/common';
|
import { BadRequestException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import { Repository } from 'typeorm';
|
import { DataSource, In, Repository } from 'typeorm';
|
||||||
import { Submission } from '../entities/submission.entity';
|
import { Submission } from '../entities/submission.entity';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { AnswerSubmissionDTO } from '../dtos/answer-submission.dto';
|
import { AnswerSubmissionDTO } from '../dtos/answer-submission.dto';
|
||||||
@@ -14,7 +14,6 @@ import { AppResponse } from '@/common/responses';
|
|||||||
import { QuestionType } from '@/modules/questions/enums/question-type.enum';
|
import { QuestionType } from '@/modules/questions/enums/question-type.enum';
|
||||||
import { NumericMetadata } from '@/modules/questions/interfaces/numeric-metadata.interface';
|
import { NumericMetadata } from '@/modules/questions/interfaces/numeric-metadata.interface';
|
||||||
import { SubmissionAnswer } from '../entities/submission-answer.entity';
|
import { SubmissionAnswer } from '../entities/submission-answer.entity';
|
||||||
import { ChoicesService } from '@/modules/questions/providers/choices.service';
|
|
||||||
import { CalculateResultProvider } from './calculate-result.provider';
|
import { CalculateResultProvider } from './calculate-result.provider';
|
||||||
import { QuestionsService } from '@/modules/questions/providers/questions.service';
|
import { QuestionsService } from '@/modules/questions/providers/questions.service';
|
||||||
import { SubmissionResult } from '../entities/submission-result.entity';
|
import { SubmissionResult } from '../entities/submission-result.entity';
|
||||||
@@ -50,15 +49,15 @@ export class AnswerSubmissionProvider {
|
|||||||
*/
|
*/
|
||||||
private readonly questionsService: QuestionsService,
|
private readonly questionsService: QuestionsService,
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject Choices Service
|
|
||||||
*/
|
|
||||||
private readonly choicesService: ChoicesService,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inject CalculateResult Provider
|
* Inject CalculateResult Provider
|
||||||
*/
|
*/
|
||||||
private readonly calculateResultProvider: CalculateResultProvider,
|
private readonly calculateResultProvider: CalculateResultProvider,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject DataSource
|
||||||
|
*/
|
||||||
|
private readonly dataSource: DataSource,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async answer(
|
public async answer(
|
||||||
@@ -109,30 +108,22 @@ export class AnswerSubmissionProvider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (submission.user.id !== user.id) {
|
const [currentQuestion, nextQuestion]: Array<Question | null> =
|
||||||
// throw new ForbiddenException('auth.errors.accessDenied');
|
await this.questionsService.findByOptionsForAdmin({
|
||||||
// }
|
|
||||||
|
|
||||||
// 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 currentQuestion: Question | null =
|
|
||||||
await this.questionsService.findOneForAdmin({
|
|
||||||
where: {
|
where: {
|
||||||
test: { id: submission.test.id },
|
test: { id: submission.test.id },
|
||||||
order: submission.currentQuestionOrder,
|
order: In([
|
||||||
|
submission.currentQuestionOrder,
|
||||||
|
submission.currentQuestionOrder + 1,
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
relations: {
|
relations: {
|
||||||
choices: true,
|
choices: true,
|
||||||
rangeRules: true,
|
rangeRules: true,
|
||||||
},
|
},
|
||||||
|
order: {
|
||||||
|
order: 'ASC',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!currentQuestion) {
|
if (!currentQuestion) {
|
||||||
@@ -174,8 +165,8 @@ export class AnswerSubmissionProvider {
|
|||||||
currentQuestion.type === QuestionType.MULTI_CHOICE &&
|
currentQuestion.type === QuestionType.MULTI_CHOICE &&
|
||||||
answerSubmissionDto.choiceId
|
answerSubmissionDto.choiceId
|
||||||
) {
|
) {
|
||||||
const choice = await this.choicesService.findOneForAdmin(
|
const choice = currentQuestion.choices.find(
|
||||||
answerSubmissionDto.choiceId,
|
(el) => el.id === answerSubmissionDto.choiceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!choice) {
|
if (!choice) {
|
||||||
@@ -189,36 +180,45 @@ export class AnswerSubmissionProvider {
|
|||||||
submissionAnswer.choice = choice;
|
submissionAnswer.choice = choice;
|
||||||
} else if (
|
} else if (
|
||||||
currentQuestion.type === QuestionType.NUMERIC &&
|
currentQuestion.type === QuestionType.NUMERIC &&
|
||||||
answerSubmissionDto.numericValue
|
answerSubmissionDto.numericValue !== undefined
|
||||||
) {
|
) {
|
||||||
submissionAnswer.numericValue = answerSubmissionDto.numericValue;
|
submissionAnswer.numericValue = answerSubmissionDto.numericValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let completedResponse: object | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.submissionAnswerRepository.save(submissionAnswer);
|
completedResponse = await this.dataSource.transaction(async (manager) => {
|
||||||
|
await manager.save(submissionAnswer);
|
||||||
|
|
||||||
|
if (!nextQuestion) {
|
||||||
|
const result = await this.calculateResultProvider.calculate(
|
||||||
|
submission,
|
||||||
|
manager,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isCompleted: true,
|
||||||
|
currentQuestion: null,
|
||||||
|
result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.increment(
|
||||||
|
Submission,
|
||||||
|
{ id: submission.id },
|
||||||
|
'currentQuestionOrder',
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new AppTimeoutException(err);
|
throw new AppTimeoutException(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextQuestion: Question | null =
|
if (completedResponse) {
|
||||||
await this.questionsService.findOneForAdmin({
|
return new AppResponse(completedResponse);
|
||||||
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,
|
|
||||||
currentQuestion: null,
|
|
||||||
result,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AppResponse({
|
return new AppResponse({
|
||||||
@@ -229,7 +229,11 @@ export class AnswerSubmissionProvider {
|
|||||||
content: nextQuestion.content,
|
content: nextQuestion.content,
|
||||||
required: nextQuestion.required,
|
required: nextQuestion.required,
|
||||||
metadata: nextQuestion.metadata,
|
metadata: nextQuestion.metadata,
|
||||||
choices: nextQuestion.choices,
|
choices: nextQuestion.choices.map((el) => ({
|
||||||
|
id: el.id,
|
||||||
|
text: el.text,
|
||||||
|
order: el.order,
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -255,46 +259,6 @@ export class AnswerSubmissionProvider {
|
|||||||
return submission;
|
return submission;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getQuestionStats(
|
|
||||||
allQuestions: Array<Question>,
|
|
||||||
answeredQuestions: Array<Question>,
|
|
||||||
) {
|
|
||||||
const questionStats: Record<'prev' | 'current' | 'next', Question | null> =
|
|
||||||
{
|
|
||||||
prev: null,
|
|
||||||
current: null,
|
|
||||||
next: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (allQuestions.length === 0) {
|
|
||||||
return questionStats;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (answeredQuestions.length > 0) {
|
|
||||||
questionStats.prev = answeredQuestions[answeredQuestions.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!questionStats.prev) {
|
|
||||||
questionStats.current = allQuestions[0];
|
|
||||||
} else {
|
|
||||||
const prevQuestionIdx = allQuestions.findIndex(
|
|
||||||
(val) => val.id === questionStats.prev?.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
questionStats.current = allQuestions[prevQuestionIdx + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (questionStats.current) {
|
|
||||||
const currentQuestionIdx = allQuestions.findIndex(
|
|
||||||
(val) => val.id === questionStats.current?.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
questionStats.next = allQuestions[currentQuestionIdx + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return questionStats;
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkAnswerCorrectness(
|
private checkAnswerCorrectness(
|
||||||
question: Question,
|
question: Question,
|
||||||
answerSubmissionDto: AnswerSubmissionDTO,
|
answerSubmissionDto: AnswerSubmissionDTO,
|
||||||
|
|||||||
@@ -1,50 +1,27 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { LessThanOrEqual, MoreThanOrEqual, Repository } from 'typeorm';
|
import { EntityManager, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
|
||||||
import { SubmissionResult } from '../entities/submission-result.entity';
|
import { SubmissionResult } from '../entities/submission-result.entity';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Submission } from '../entities/submission.entity';
|
import { Submission } from '../entities/submission.entity';
|
||||||
import { QuestionType } from '@/modules/questions/enums/question-type.enum';
|
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 { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
|
||||||
import { ResultRangeService } from '@/modules/scoring/providers/result-range.service';
|
|
||||||
import { SubmissionAnswer } from '../entities/submission-answer.entity';
|
import { SubmissionAnswer } from '../entities/submission-answer.entity';
|
||||||
|
import { Question } from '@/modules/questions/entities/question.entity';
|
||||||
|
import { ResultRange } from '@/modules/scoring/entities/result-range.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CalculateResultProvider {
|
export class CalculateResultProvider {
|
||||||
constructor(
|
public async calculate(submission: Submission, manager: EntityManager) {
|
||||||
/**
|
|
||||||
* Inject SubmissionResult Repository
|
|
||||||
*/
|
|
||||||
@InjectRepository(SubmissionResult)
|
|
||||||
private readonly submissionResultRepository: Repository<SubmissionResult>,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject Questions Service
|
|
||||||
*/
|
|
||||||
private readonly questionsService: QuestionsService,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject ResultRange Service
|
|
||||||
*/
|
|
||||||
private readonly resultRangeService: ResultRangeService,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject SubmissionAnswer Repository
|
|
||||||
*/
|
|
||||||
@InjectRepository(SubmissionAnswer)
|
|
||||||
private readonly submissionAnswerRepository: Repository<SubmissionAnswer>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async calculate(submission: Submission) {
|
|
||||||
let score = 0;
|
let score = 0;
|
||||||
|
|
||||||
let submissionAnswers: Array<SubmissionAnswer> = [];
|
let submissionAnswers: Array<SubmissionAnswer> = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
submissionAnswers = await this.submissionAnswerRepository.find({
|
submissionAnswers = await manager.find(SubmissionAnswer, {
|
||||||
where: { submission: { id: submission.id } },
|
where: { submission: { id: submission.id } },
|
||||||
relations: {
|
relations: {
|
||||||
question: true,
|
question: {
|
||||||
|
rangeRules: true,
|
||||||
|
},
|
||||||
choice: true,
|
choice: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -52,39 +29,33 @@ export class CalculateResultProvider {
|
|||||||
throw new AppTimeoutException(err);
|
throw new AppTimeoutException(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(
|
for (const answer of submissionAnswers) {
|
||||||
submissionAnswers.map(async (answer) => {
|
if (answer.question.type === QuestionType.MULTI_CHOICE && answer.choice) {
|
||||||
|
score += answer.question.weight * answer.choice.score;
|
||||||
|
} else if (
|
||||||
|
answer.question.type === QuestionType.NUMERIC &&
|
||||||
|
answer.numericValue !== undefined
|
||||||
|
) {
|
||||||
if (
|
if (
|
||||||
answer.question.type === QuestionType.MULTI_CHOICE &&
|
answer.question.rangeRules &&
|
||||||
answer.choice
|
answer.question.rangeRules.length > 0
|
||||||
) {
|
) {
|
||||||
score += answer.question.weight * answer.choice.score;
|
answer.question.rangeRules.forEach((rule) => {
|
||||||
} else if (
|
if (
|
||||||
answer.question.type === QuestionType.NUMERIC &&
|
answer.numericValue !== undefined &&
|
||||||
answer.numericValue !== undefined
|
answer.numericValue >= rule.min &&
|
||||||
) {
|
answer.numericValue <= rule.max
|
||||||
const question = await this.questionsService.findOneByIdForAdmin(
|
) {
|
||||||
answer.question.id,
|
score += answer.question.weight * rule.score;
|
||||||
);
|
}
|
||||||
|
});
|
||||||
if (question.rangeRules && question.rangeRules.length > 0) {
|
|
||||||
question.rangeRules.forEach((rule) => {
|
|
||||||
if (
|
|
||||||
answer.numericValue &&
|
|
||||||
answer.numericValue >= rule.min &&
|
|
||||||
answer.numericValue <= rule.max
|
|
||||||
) {
|
|
||||||
score += answer.question.weight * rule.score;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
let totalScores = 0;
|
let totalScores = 0;
|
||||||
|
|
||||||
const testQuestions = await this.questionsService.findByOptionsForAdmin({
|
const testQuestions = await manager.find(Question, {
|
||||||
where: { test: { id: submission.test.id } },
|
where: { test: { id: submission.test.id } },
|
||||||
relations: {
|
relations: {
|
||||||
choices: true,
|
choices: true,
|
||||||
@@ -113,14 +84,15 @@ export class CalculateResultProvider {
|
|||||||
totalScores += question.weight * maxScore;
|
totalScores += question.weight * maxScore;
|
||||||
});
|
});
|
||||||
|
|
||||||
const resultRanges = await this.resultRangeService.findByOptions({
|
const resultRanges = await manager.find(ResultRange, {
|
||||||
where: {
|
where: {
|
||||||
|
test: { id: submission.test.id },
|
||||||
minScore: LessThanOrEqual(score),
|
minScore: LessThanOrEqual(score),
|
||||||
maxScore: MoreThanOrEqual(score),
|
maxScore: MoreThanOrEqual(score),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const submissionResult = this.submissionResultRepository.create({
|
const submissionResult = manager.create(SubmissionResult, {
|
||||||
submission,
|
submission,
|
||||||
score,
|
score,
|
||||||
probability: Math.ceil((score / totalScores) * 100),
|
probability: Math.ceil((score / totalScores) * 100),
|
||||||
@@ -128,7 +100,7 @@ export class CalculateResultProvider {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.submissionResultRepository.save(submissionResult);
|
await manager.save(submissionResult);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new AppTimeoutException(err);
|
throw new AppTimeoutException(err);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user