import { PatternValidation, UpkQuestionConfigurationMC, UpkQuestionConfigurationTE } from "@/api_fsb"; import { assert } from "@/lib/utils"; import { ProfileQuestion, ValidationError } from '@/models/profilingQuestionsSlice'; import { Filter } from 'bad-words'; const filter = new Filter(); export const questionUtils = { assignValidationState: (question: ProfileQuestion): ProfileQuestion => { /* This function performs validation on the question's current answers and assigns any error messages to question._metadata.error_msg. If the question is MC, validate: */ const errors = question._validation.errors ?? [] question._validation.isComplete = errors.length === 0 question._validation.isValid = errors.filter(e => e.severity === 'error').length === 0 return question; }, validate: (question: ProfileQuestion): ProfileQuestion => { /* If the question is MC, validate: - validate selector SA vs MA (1 selected vs >1 selected) - the answers match actual codes in the choices - validate configuration.max_select - validate choices.exclusive If the question is TE, validate that: - configuration.max_length - validation.patterns */ assert(question.question_id, "Question must have question_id") // assert(question.configuration, "Question must have configuration") const answers = question._metadata.answers ?? [] // Declare it here, and they we'll reassign any specific // configuration types based on the question type. // Start fresh without any error messages before validating const errors: ValidationError[] = [] switch (question.question_type) { case "TE": answers.length === 0 && errors.push({ path: `question_id:${question.question_id}`, message: "No answer provided", severity: "warning", } as ValidationError) answers.length > 1 && errors.push({ path: `question_id:${question.question_id}`, message: "Only one answer allowed", severity: "error", } as ValidationError) let answer_text: string = answers[0] const te_config = question.configuration as UpkQuestionConfigurationTE const min_length = te_config.min_length ?? 0 const max_length = te_config.max_length ?? 100_000 answer_text.length < min_length && errors.push({ path: `question_id:${question.question_id}`, message: "Answer shorter than allowed", severity: "warning", } as ValidationError) answer_text.length > max_length && errors.push({ path: `question_id:${question.question_id}`, message: "Answer longer than allowed", severity: "error", } as ValidationError) filter.isProfane(answer_text) && errors.push({ path: `question_id:${question.question_id}`, message: "Answer has inappropriate content", severity: "error", } as ValidationError) const patterns: PatternValidation[] = (question.validation ?? {})["patterns"] ?? [] patterns.forEach((pv) => { let re = new RegExp(pv.pattern) answer_text.search(re) == -1 && errors.push({ path: `question_id:${question.question_id}`, message: pv.message, severity: "error", } as ValidationError) }) break; case "MC": answers.length === 0 && errors.push({ path: `question_id:${question.question_id}`, message: "Multiple Choice question with no selected answers", severity: "warning", } as ValidationError) const choice_codes: string[] = question.choices?.map((c) => c.choice_id) ?? []; const mc_config = (question.configuration ?? {}) as UpkQuestionConfigurationMC const max_select = mc_config.max_select ?? choice_codes.length switch (question.selector) { case "SA": answers.length > 1 && errors.push({ path: `question_id:${question.question_id}`, message: "Single Answer MC question with >1 selected answers", severity: "error", } as ValidationError) break; case "MA": answers.length > max_select && errors.push({ path: `question_id:${question.question_id}`, message: "More options selected than allowed", severity: "error", } as ValidationError) answers.length === max_select && errors.push({ path: `question_id:${question.question_id}`, message: "Selected all the options", severity: "error", } as ValidationError) break; } // Now validate Multiple Choice answers regardless of if // they're Single Answer (SA) or Multiple Answer (MA) !answers.every(item => choice_codes.includes(item)) && errors.push({ path: `question_id:${question.question_id}`, message: "Invalid options selected", severity: "error", }) // const max_select: number = question.configuration?.max_select ?? choice_codes.length // if (answer.values.length > max_select) { // answer.error_msg = "More options selected than allowed" // } /* exclusive_choice = next((x for x in question["choices"] if x.get("exclusive")), None) if exclusive_choice: exclusive_choice_id = exclusive_choice["choice_id"] assert answer == [exclusive_choice_id] or \ exclusive_choice_id not in answer, "Invalid exclusive selection" */ break; default: errors.push({ path: `question_id:${question.question_id}`, message: "Incorrect Question Type provided", severity: "error", } as ValidationError) } // Assign the errors back to the question's _validation field and // then return the question // TODO: does this edit in place, maybe I don't need to return it question._validation.errors = errors return question; }, };