feat: add multi choice submission logic
This commit is contained in:
@@ -2,6 +2,8 @@ import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
@@ -29,6 +31,10 @@ export class SubmissionAnswer {
|
||||
@ManyToOne(() => Choice, { nullable: true, onDelete: 'CASCADE' })
|
||||
choice?: Choice;
|
||||
|
||||
@ManyToMany(() => Choice)
|
||||
@JoinTable()
|
||||
choices?: Array<Choice>;
|
||||
|
||||
@Column({ type: 'int', nullable: true })
|
||||
numericValue?: number;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export const checkAnswerCorrectness = (
|
||||
) => {
|
||||
if (!answerSubmissionDto) throw new BadRequestException();
|
||||
|
||||
const { choiceId, numericValue } = answerSubmissionDto;
|
||||
const { choiceId, choiceIds, numericValue } = answerSubmissionDto;
|
||||
|
||||
if (question.type === QuestionType.NUMERIC) {
|
||||
if (numericValue === undefined) {
|
||||
@@ -28,15 +28,32 @@ export const checkAnswerCorrectness = (
|
||||
}
|
||||
}
|
||||
|
||||
const questionChoiceIds = question.choices.map((el) => el.id);
|
||||
|
||||
if (question.type === QuestionType.SINGLE_CHOICE) {
|
||||
if (choiceId === undefined || choiceId === null) {
|
||||
throw new BadRequestException('submissions.errors.emptyChoiceId');
|
||||
}
|
||||
|
||||
if (!question.choices?.some((c) => c.id === choiceId)) {
|
||||
if (!questionChoiceIds.some((c) => c === choiceId)) {
|
||||
throw new AppException('validation.wrongField', HttpStatus.BAD_REQUEST, {
|
||||
field: '$t(submissions.fields.choiceId)',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (question.type === QuestionType.MULTI_CHOICE) {
|
||||
if (choiceIds === undefined || choiceIds === null) {
|
||||
throw new BadRequestException('submissions.errors.emptyChoiceId');
|
||||
}
|
||||
|
||||
if (
|
||||
choiceIds.length > 0 &&
|
||||
!choiceIds.every((el) => questionChoiceIds.includes(el))
|
||||
) {
|
||||
throw new AppException('validation.wrongField', HttpStatus.BAD_REQUEST, {
|
||||
field: '$t(submissions.fields.choiceIds)',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ import { SingleChoiceCondition } from '@/modules/questions/interfaces/single-cho
|
||||
import { NumericCondition } from '@/modules/questions/interfaces/numeric-condition.interface';
|
||||
import { AppException } from '@/common/exceptions/app.exception';
|
||||
import { HttpStatus } from '@nestjs/common';
|
||||
import { MultiChoiceCondition } from '@/modules/questions/interfaces/multi-choice-condition.interface';
|
||||
import { MultiChoiceTConditionOperator } from '@/modules/questions/enums/multi-choice-tcondition-operator.enum';
|
||||
|
||||
export const getNextTransition = (
|
||||
question: Question,
|
||||
@@ -31,6 +33,57 @@ export const getNextTransition = (
|
||||
if (transition) return transition;
|
||||
}
|
||||
|
||||
if (
|
||||
question.type === QuestionType.MULTI_CHOICE &&
|
||||
answerSubmissionDto.choiceIds !== undefined
|
||||
) {
|
||||
const selectedChoiceIds = new Set(answerSubmissionDto.choiceIds);
|
||||
|
||||
const transition = transitions.find((t) => {
|
||||
if (t.type !== QuestionTransitionType.MULTI_CHOICE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const condition = t.condition as MultiChoiceCondition;
|
||||
|
||||
if (
|
||||
!condition ||
|
||||
!Array.isArray(condition.choiceIds) ||
|
||||
!condition.operator
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const conditionChoiceIds = new Set(condition.choiceIds);
|
||||
|
||||
switch (condition.operator) {
|
||||
case MultiChoiceTConditionOperator.ANY:
|
||||
return [...conditionChoiceIds].some((id) =>
|
||||
selectedChoiceIds.has(id),
|
||||
);
|
||||
|
||||
case MultiChoiceTConditionOperator.ALL:
|
||||
return [...conditionChoiceIds].every((id) =>
|
||||
selectedChoiceIds.has(id),
|
||||
);
|
||||
|
||||
case MultiChoiceTConditionOperator.EXACT:
|
||||
if (selectedChoiceIds.size !== conditionChoiceIds.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [...conditionChoiceIds].every((id) =>
|
||||
selectedChoiceIds.has(id),
|
||||
);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (transition) return transition;
|
||||
}
|
||||
|
||||
if (
|
||||
question.type === QuestionType.NUMERIC &&
|
||||
answerSubmissionDto.numericValue !== undefined
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export interface GuestSubmissionAnswer {
|
||||
questionId: number;
|
||||
choiceId?: number;
|
||||
choiceIds?: Array<number>;
|
||||
numericValue?: number;
|
||||
}
|
||||
|
||||
@@ -147,6 +147,15 @@ export class AnswerAuthenticatedProvider {
|
||||
}
|
||||
|
||||
submissionAnswer.choice = choice;
|
||||
} else if (
|
||||
currentQuestion.type === QuestionType.MULTI_CHOICE &&
|
||||
answerSubmissionDto.choiceIds !== undefined
|
||||
) {
|
||||
const choices = currentQuestion.choices.filter((el) =>
|
||||
answerSubmissionDto.choiceIds?.includes(el.id),
|
||||
);
|
||||
|
||||
submissionAnswer.choices = choices;
|
||||
} else if (
|
||||
currentQuestion.type === QuestionType.NUMERIC &&
|
||||
answerSubmissionDto.numericValue !== undefined
|
||||
|
||||
@@ -56,7 +56,7 @@ export class AnswerGuestProvider {
|
||||
}
|
||||
|
||||
try {
|
||||
return this.answerGuest(id, answerSubmissionDto);
|
||||
return await this.answerGuest(id, answerSubmissionDto);
|
||||
} finally {
|
||||
await this.redisService.releaseLock(lockKey, lockToken);
|
||||
}
|
||||
@@ -142,6 +142,15 @@ export class AnswerGuestProvider {
|
||||
}
|
||||
|
||||
newAnswer.choiceId = choice.id;
|
||||
} else if (
|
||||
currentQuestion.type === QuestionType.MULTI_CHOICE &&
|
||||
answerSubmissionDto.choiceIds !== undefined
|
||||
) {
|
||||
const choiceIds = currentQuestion.choices
|
||||
.filter((el) => answerSubmissionDto.choiceIds?.includes(el.id))
|
||||
.map((el) => el.id);
|
||||
|
||||
newAnswer.choiceIds = [...new Set(choiceIds)].sort((a, b) => a - b);
|
||||
} else if (
|
||||
currentQuestion.type === QuestionType.NUMERIC &&
|
||||
answerSubmissionDto.numericValue !== undefined
|
||||
|
||||
@@ -47,6 +47,7 @@ export class CalculateResultProvider {
|
||||
rangeRules: true,
|
||||
},
|
||||
choice: true,
|
||||
choices: true,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -59,6 +60,8 @@ export class CalculateResultProvider {
|
||||
question: answer.question,
|
||||
choiceId: answer.choice?.id,
|
||||
choice: answer.choice,
|
||||
choiceIds: answer.choices?.map((choice) => choice.id),
|
||||
choices: answer.choices,
|
||||
numericValue: answer.numericValue,
|
||||
})),
|
||||
async (questionIds) =>
|
||||
@@ -105,6 +108,7 @@ export class CalculateResultProvider {
|
||||
submission.answers.map((answer) => ({
|
||||
questionId: answer.questionId,
|
||||
choiceId: answer.choiceId,
|
||||
choiceIds: answer.choiceIds,
|
||||
numericValue: answer.numericValue,
|
||||
})),
|
||||
async (questionIds) =>
|
||||
@@ -141,6 +145,8 @@ export class CalculateResultProvider {
|
||||
question?: Question;
|
||||
choiceId?: number;
|
||||
choice?: { id: number; score: number };
|
||||
choiceIds?: Array<number>;
|
||||
choices?: Array<{ id: number; score: number }>;
|
||||
numericValue?: number;
|
||||
}>,
|
||||
loadQuestions: (questionIds: number[]) => Promise<Question[]>,
|
||||
@@ -172,6 +178,22 @@ export class CalculateResultProvider {
|
||||
if (choice) {
|
||||
score += question.weight * choice.score;
|
||||
}
|
||||
} else if (
|
||||
question.type === QuestionType.MULTI_CHOICE &&
|
||||
answer.choiceIds !== undefined
|
||||
) {
|
||||
const selectedChoiceIds = new Set(answer.choiceIds);
|
||||
|
||||
const selectedChoices =
|
||||
answer.choices ??
|
||||
question.choices.filter((choice) => selectedChoiceIds.has(choice.id));
|
||||
|
||||
const multiChoiceScore = selectedChoices.reduce(
|
||||
(sum, choice) => sum + choice.score,
|
||||
0,
|
||||
);
|
||||
|
||||
score += question.weight * multiChoiceScore;
|
||||
} else if (
|
||||
question.type === QuestionType.NUMERIC &&
|
||||
answer.numericValue !== undefined
|
||||
@@ -190,25 +212,31 @@ export class CalculateResultProvider {
|
||||
let totalScores = 0;
|
||||
|
||||
questions.forEach((question) => {
|
||||
const answerScores: number[] = [];
|
||||
|
||||
if (question.type === QuestionType.SINGLE_CHOICE) {
|
||||
question.choices.forEach((choice) => {
|
||||
answerScores.push(choice.score);
|
||||
});
|
||||
const maxScore =
|
||||
question.choices.length > 0
|
||||
? Math.max(...question.choices.map((choice) => choice.score))
|
||||
: 0;
|
||||
|
||||
totalScores += question.weight * maxScore;
|
||||
} else if (question.type === QuestionType.MULTI_CHOICE) {
|
||||
const maxScore = question.choices.reduce(
|
||||
(sum, choice) => sum + choice.score,
|
||||
0,
|
||||
);
|
||||
|
||||
totalScores += question.weight * maxScore;
|
||||
} else if (
|
||||
question.type === QuestionType.NUMERIC &&
|
||||
question.rangeRules &&
|
||||
question.rangeRules.length > 0
|
||||
) {
|
||||
question.rangeRules.forEach((rule) => {
|
||||
answerScores.push(rule.score);
|
||||
});
|
||||
const maxScore = Math.max(
|
||||
...question.rangeRules.map((rule) => rule.score),
|
||||
);
|
||||
|
||||
totalScores += question.weight * maxScore;
|
||||
}
|
||||
|
||||
const maxScore = answerScores.length > 0 ? Math.max(...answerScores) : 0;
|
||||
|
||||
totalScores += question.weight * maxScore;
|
||||
});
|
||||
|
||||
const resultRanges = await loadResultRanges(score);
|
||||
|
||||
Reference in New Issue
Block a user