summaryrefslogtreecommitdiff
path: root/jb-ui/src/models/profilingUtils.ts
diff options
context:
space:
mode:
Diffstat (limited to 'jb-ui/src/models/profilingUtils.ts')
-rw-r--r--jb-ui/src/models/profilingUtils.ts177
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