feat: add multi choice submission logic
This commit is contained in:
@@ -4,7 +4,8 @@
|
|||||||
"questionId": "Question's Id",
|
"questionId": "Question's Id",
|
||||||
"submission": "Submission",
|
"submission": "Submission",
|
||||||
"submissionId": "Submission's Id",
|
"submissionId": "Submission's Id",
|
||||||
"choiceId": "Choice's Id"
|
"choiceId": "Choice's Id",
|
||||||
|
"choiceIds": "Choice Ids list"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"alreadyTookTest": "You already took this test.",
|
"alreadyTookTest": "You already took this test.",
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
"emptyNumericValue": "A numeric value should be provided as answer.",
|
"emptyNumericValue": "A numeric value should be provided as answer.",
|
||||||
"numericValueRange": "Numeric value is out of specified range.",
|
"numericValueRange": "Numeric value is out of specified range.",
|
||||||
"emptyChoiceId": "Choice Id should be provided as answer.",
|
"emptyChoiceId": "Choice Id should be provided as answer.",
|
||||||
|
"emptyChoiceIds": "Choice Ids should be provided as answer.",
|
||||||
"transitionNotFound": "No transition found for the test",
|
"transitionNotFound": "No transition found for the test",
|
||||||
"uniqueSubmissionQuestion": "Question's Answer already exists."
|
"uniqueSubmissionQuestion": "Question's Answer already exists."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"questionId": "شناسه سوال",
|
"questionId": "شناسه سوال",
|
||||||
"submission": "Submission",
|
"submission": "Submission",
|
||||||
"submissionId": "شناسه Submission",
|
"submissionId": "شناسه Submission",
|
||||||
"choiceId": "شناسه گزینه"
|
"choiceId": "شناسه گزینه",
|
||||||
|
"choiceIds": "لیست شناسه گزینه ها"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"alreadyTookTest": "قبلا این آزمون را انجام داده اید.",
|
"alreadyTookTest": "قبلا این آزمون را انجام داده اید.",
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
"emptyNumericValue": "یک مقدار عددی باید به عنوان پاسخ سوال مشخص شود.",
|
"emptyNumericValue": "یک مقدار عددی باید به عنوان پاسخ سوال مشخص شود.",
|
||||||
"numericValueRange": "مقدار عددی خارج از بازه مشخص شده است.",
|
"numericValueRange": "مقدار عددی خارج از بازه مشخص شده است.",
|
||||||
"emptyChoiceId": "شناسه گزینه باید به عنوان پاسخ مشخص شود.",
|
"emptyChoiceId": "شناسه گزینه باید به عنوان پاسخ مشخص شود.",
|
||||||
|
"emptyChoiceIds": "لیستی از شناسه گزینه ها باید به عنوان پاسخ مشخص شود.",
|
||||||
"transitionNotFound": "مسیری برای تست پیدا نشد.",
|
"transitionNotFound": "مسیری برای تست پیدا نشد.",
|
||||||
"uniqueSubmissionQuestion": "این سوال قبلا پاسخ داده شده است."
|
"uniqueSubmissionQuestion": "این سوال قبلا پاسخ داده شده است."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ export class SubmissionCurrentQuestion1786282238898 implements MigrationInterfac
|
|||||||
name = 'SubmissionCurrentQuestion1786282238898';
|
name = 'SubmissionCurrentQuestion1786282238898';
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "public"."unq_test_start"`);
|
await queryRunner.query(`DROP INDEX IF EXISTS "public"."unq_test_start"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."unq_question_end"`);
|
await queryRunner.query(`DROP INDEX IF EXISTS "public"."unq_question_end"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."unq_question_default"`);
|
await queryRunner.query(
|
||||||
await queryRunner.query(`DROP INDEX "public"."unq_question_choice"`);
|
`DROP INDEX IF EXISTS "public"."unq_question_default"`,
|
||||||
await queryRunner.query(`DROP INDEX "public"."unq_question_number_range"`);
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX IF EXISTS "public"."unq_question_choice"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX IF EXISTS "public"."unq_question_number_range"`,
|
||||||
|
);
|
||||||
await queryRunner.query(
|
await queryRunner.query(
|
||||||
`ALTER TABLE "question_transition" DROP CONSTRAINT "ex_question_number_range_overlap"`,
|
`ALTER TABLE "question_transition" DROP CONSTRAINT "ex_question_number_range_overlap"`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class MultiChoiceSubmission1787294625853 implements MigrationInterface {
|
||||||
|
name = 'MultiChoiceSubmission1787294625853';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX "public"."IDX_ea78aac2e76e03f1c2fc720f1d"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "submission_answer_choices_choice" ("submissionAnswerId" integer NOT NULL, "choiceId" integer NOT NULL, CONSTRAINT "PK_d3154f459415dc8f97a4bb10a1e" PRIMARY KEY ("submissionAnswerId", "choiceId"))`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_6f1902623cd8b3b52a5688a949" ON "submission_answer_choices_choice" ("submissionAnswerId") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE INDEX "IDX_2e699b5f339fb37917549c2fbf" ON "submission_answer_choices_choice" ("choiceId") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TYPE "public"."question_transition_type_enum" RENAME TO "question_transition_type_enum_old"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TYPE "public"."question_transition_type_enum" AS ENUM('start', 'default', 'choice', 'multi-choice', 'number-range', 'end')`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "question_transition" ALTER COLUMN "type" TYPE "public"."question_transition_type_enum" USING "type"::"text"::"public"."question_transition_type_enum"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP TYPE "public"."question_transition_type_enum_old"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TYPE "public"."question_type_enum" RENAME TO "question_type_enum_old"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TYPE "public"."question_type_enum" AS ENUM('single-choice', 'multi-choice', 'numeric', 'text')`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "question" ALTER COLUMN "type" DROP DEFAULT`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "question" ALTER COLUMN "type" TYPE "public"."question_type_enum" USING "type"::"text"::"public"."question_type_enum"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "question" ALTER COLUMN "type" SET DEFAULT 'single-choice'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TYPE "public"."question_type_enum_old"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE UNIQUE INDEX "UNQ_SUBMISSION_QUESTION_IDX" ON "submission_answer" ("submissionId", "questionId") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "submission_answer_choices_choice" ADD CONSTRAINT "FK_6f1902623cd8b3b52a5688a949b" FOREIGN KEY ("submissionAnswerId") REFERENCES "submission_answer"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "submission_answer_choices_choice" ADD CONSTRAINT "FK_2e699b5f339fb37917549c2fbfc" FOREIGN KEY ("choiceId") REFERENCES "choice"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "submission_answer_choices_choice" DROP CONSTRAINT "FK_2e699b5f339fb37917549c2fbfc"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "submission_answer_choices_choice" DROP CONSTRAINT "FK_6f1902623cd8b3b52a5688a949b"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX "public"."UNQ_SUBMISSION_QUESTION_IDX"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TYPE "public"."question_type_enum_old" AS ENUM('multi-choice', 'numeric', 'text')`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "question" ALTER COLUMN "type" DROP DEFAULT`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "question" ALTER COLUMN "type" TYPE "public"."question_type_enum_old" USING "type"::"text"::"public"."question_type_enum_old"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "question" ALTER COLUMN "type" SET DEFAULT 'multi-choice'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TYPE "public"."question_type_enum"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TYPE "public"."question_type_enum_old" RENAME TO "question_type_enum"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TYPE "public"."question_transition_type_enum_old" AS ENUM('start', 'default', 'choice', 'number-range', 'end')`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "question_transition" ALTER COLUMN "type" TYPE "public"."question_transition_type_enum_old" USING "type"::"text"::"public"."question_transition_type_enum_old"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP TYPE "public"."question_transition_type_enum"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TYPE "public"."question_transition_type_enum_old" RENAME TO "question_transition_type_enum"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX "public"."IDX_2e699b5f339fb37917549c2fbf"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`DROP INDEX "public"."IDX_6f1902623cd8b3b52a5688a949"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TABLE "submission_answer_choices_choice"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE UNIQUE INDEX "IDX_ea78aac2e76e03f1c2fc720f1d" ON "submission_answer" ("questionId", "submissionId") `,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
Entity,
|
Entity,
|
||||||
Index,
|
Index,
|
||||||
|
JoinTable,
|
||||||
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
@@ -29,6 +31,10 @@ export class SubmissionAnswer {
|
|||||||
@ManyToOne(() => Choice, { nullable: true, onDelete: 'CASCADE' })
|
@ManyToOne(() => Choice, { nullable: true, onDelete: 'CASCADE' })
|
||||||
choice?: Choice;
|
choice?: Choice;
|
||||||
|
|
||||||
|
@ManyToMany(() => Choice)
|
||||||
|
@JoinTable()
|
||||||
|
choices?: Array<Choice>;
|
||||||
|
|
||||||
@Column({ type: 'int', nullable: true })
|
@Column({ type: 'int', nullable: true })
|
||||||
numericValue?: number;
|
numericValue?: number;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const checkAnswerCorrectness = (
|
|||||||
) => {
|
) => {
|
||||||
if (!answerSubmissionDto) throw new BadRequestException();
|
if (!answerSubmissionDto) throw new BadRequestException();
|
||||||
|
|
||||||
const { choiceId, numericValue } = answerSubmissionDto;
|
const { choiceId, choiceIds, numericValue } = answerSubmissionDto;
|
||||||
|
|
||||||
if (question.type === QuestionType.NUMERIC) {
|
if (question.type === QuestionType.NUMERIC) {
|
||||||
if (numericValue === undefined) {
|
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 (question.type === QuestionType.SINGLE_CHOICE) {
|
||||||
if (choiceId === undefined || choiceId === null) {
|
if (choiceId === undefined || choiceId === null) {
|
||||||
throw new BadRequestException('submissions.errors.emptyChoiceId');
|
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, {
|
throw new AppException('validation.wrongField', HttpStatus.BAD_REQUEST, {
|
||||||
field: '$t(submissions.fields.choiceId)',
|
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 { NumericCondition } from '@/modules/questions/interfaces/numeric-condition.interface';
|
||||||
import { AppException } from '@/common/exceptions/app.exception';
|
import { AppException } from '@/common/exceptions/app.exception';
|
||||||
import { HttpStatus } from '@nestjs/common';
|
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 = (
|
export const getNextTransition = (
|
||||||
question: Question,
|
question: Question,
|
||||||
@@ -31,6 +33,57 @@ export const getNextTransition = (
|
|||||||
if (transition) return transition;
|
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 (
|
if (
|
||||||
question.type === QuestionType.NUMERIC &&
|
question.type === QuestionType.NUMERIC &&
|
||||||
answerSubmissionDto.numericValue !== undefined
|
answerSubmissionDto.numericValue !== undefined
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export interface GuestSubmissionAnswer {
|
export interface GuestSubmissionAnswer {
|
||||||
questionId: number;
|
questionId: number;
|
||||||
choiceId?: number;
|
choiceId?: number;
|
||||||
|
choiceIds?: Array<number>;
|
||||||
numericValue?: number;
|
numericValue?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,15 @@ export class AnswerAuthenticatedProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submissionAnswer.choice = choice;
|
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 (
|
} else if (
|
||||||
currentQuestion.type === QuestionType.NUMERIC &&
|
currentQuestion.type === QuestionType.NUMERIC &&
|
||||||
answerSubmissionDto.numericValue !== undefined
|
answerSubmissionDto.numericValue !== undefined
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class AnswerGuestProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return this.answerGuest(id, answerSubmissionDto);
|
return await this.answerGuest(id, answerSubmissionDto);
|
||||||
} finally {
|
} finally {
|
||||||
await this.redisService.releaseLock(lockKey, lockToken);
|
await this.redisService.releaseLock(lockKey, lockToken);
|
||||||
}
|
}
|
||||||
@@ -142,6 +142,15 @@ export class AnswerGuestProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newAnswer.choiceId = choice.id;
|
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 (
|
} else if (
|
||||||
currentQuestion.type === QuestionType.NUMERIC &&
|
currentQuestion.type === QuestionType.NUMERIC &&
|
||||||
answerSubmissionDto.numericValue !== undefined
|
answerSubmissionDto.numericValue !== undefined
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export class CalculateResultProvider {
|
|||||||
rangeRules: true,
|
rangeRules: true,
|
||||||
},
|
},
|
||||||
choice: true,
|
choice: true,
|
||||||
|
choices: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -59,6 +60,8 @@ export class CalculateResultProvider {
|
|||||||
question: answer.question,
|
question: answer.question,
|
||||||
choiceId: answer.choice?.id,
|
choiceId: answer.choice?.id,
|
||||||
choice: answer.choice,
|
choice: answer.choice,
|
||||||
|
choiceIds: answer.choices?.map((choice) => choice.id),
|
||||||
|
choices: answer.choices,
|
||||||
numericValue: answer.numericValue,
|
numericValue: answer.numericValue,
|
||||||
})),
|
})),
|
||||||
async (questionIds) =>
|
async (questionIds) =>
|
||||||
@@ -105,6 +108,7 @@ export class CalculateResultProvider {
|
|||||||
submission.answers.map((answer) => ({
|
submission.answers.map((answer) => ({
|
||||||
questionId: answer.questionId,
|
questionId: answer.questionId,
|
||||||
choiceId: answer.choiceId,
|
choiceId: answer.choiceId,
|
||||||
|
choiceIds: answer.choiceIds,
|
||||||
numericValue: answer.numericValue,
|
numericValue: answer.numericValue,
|
||||||
})),
|
})),
|
||||||
async (questionIds) =>
|
async (questionIds) =>
|
||||||
@@ -141,6 +145,8 @@ export class CalculateResultProvider {
|
|||||||
question?: Question;
|
question?: Question;
|
||||||
choiceId?: number;
|
choiceId?: number;
|
||||||
choice?: { id: number; score: number };
|
choice?: { id: number; score: number };
|
||||||
|
choiceIds?: Array<number>;
|
||||||
|
choices?: Array<{ id: number; score: number }>;
|
||||||
numericValue?: number;
|
numericValue?: number;
|
||||||
}>,
|
}>,
|
||||||
loadQuestions: (questionIds: number[]) => Promise<Question[]>,
|
loadQuestions: (questionIds: number[]) => Promise<Question[]>,
|
||||||
@@ -172,6 +178,22 @@ export class CalculateResultProvider {
|
|||||||
if (choice) {
|
if (choice) {
|
||||||
score += question.weight * choice.score;
|
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 (
|
} else if (
|
||||||
question.type === QuestionType.NUMERIC &&
|
question.type === QuestionType.NUMERIC &&
|
||||||
answer.numericValue !== undefined
|
answer.numericValue !== undefined
|
||||||
@@ -190,25 +212,31 @@ export class CalculateResultProvider {
|
|||||||
let totalScores = 0;
|
let totalScores = 0;
|
||||||
|
|
||||||
questions.forEach((question) => {
|
questions.forEach((question) => {
|
||||||
const answerScores: number[] = [];
|
|
||||||
|
|
||||||
if (question.type === QuestionType.SINGLE_CHOICE) {
|
if (question.type === QuestionType.SINGLE_CHOICE) {
|
||||||
question.choices.forEach((choice) => {
|
const maxScore =
|
||||||
answerScores.push(choice.score);
|
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 (
|
} else if (
|
||||||
question.type === QuestionType.NUMERIC &&
|
question.type === QuestionType.NUMERIC &&
|
||||||
question.rangeRules &&
|
question.rangeRules &&
|
||||||
question.rangeRules.length > 0
|
question.rangeRules.length > 0
|
||||||
) {
|
) {
|
||||||
question.rangeRules.forEach((rule) => {
|
const maxScore = Math.max(
|
||||||
answerScores.push(rule.score);
|
...question.rangeRules.map((rule) => rule.score),
|
||||||
});
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const maxScore = answerScores.length > 0 ? Math.max(...answerScores) : 0;
|
|
||||||
|
|
||||||
totalScores += question.weight * maxScore;
|
totalScores += question.weight * maxScore;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const resultRanges = await loadResultRanges(score);
|
const resultRanges = await loadResultRanges(score);
|
||||||
|
|||||||
Reference in New Issue
Block a user