diff options
Diffstat (limited to 'jb-ui/src/models/profilingUtils.ts')
| -rw-r--r-- | jb-ui/src/models/profilingUtils.ts | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/jb-ui/src/models/profilingUtils.ts b/jb-ui/src/models/profilingUtils.ts new file mode 100644 index 0000000..104a5bb --- /dev/null +++ b/jb-ui/src/models/profilingUtils.ts @@ -0,0 +1,177 @@ +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; + }, + +};
\ No newline at end of file |
