1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
|
import {createSelector, createSlice, PayloadAction} from '@reduxjs/toolkit'
// import {Answer} from "@/models/answer.ts";
// import {stringify} from "querystring";
import {RootState} from '@/store'; // your root state type
import {PatternValidation, UpkQuestion} from "@/api";
export interface Answer {
values: string[];
error_msg: string;
complete: boolean;
processing: boolean;
}
type AnswersState = {
[id: string]: Answer;
};
const initialState: AnswersState = {};
const answerSlice = createSlice({
name: 'answers',
initialState,
reducers: {
addAnswer(state, action: PayloadAction<{ question: UpkQuestion, val: string }>) {
/* 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
*/
let question: UpkQuestion = action.payload.question;
let val: string = action.payload.val.trim();
let answer: Answer = state[question.question_id] ?? {
values: [],
error_msg: "",
complete: false,
processing: false
} as Answer;
answer.error_msg = "" // Reset any error messages
switch (question.question_type) {
case "TE":
answer.values = [val]
if (answer.values.length > 1) {
answer.error_msg = "Only one answer allowed"
break;
}
let answer_text: string = answer.values[0]
if (answer_text.length <= 0) {
answer.error_msg = "Must provide answer"
break;
}
const max_length: number = (question.configuration ?? {})["max_length"] ?? 100000
if (answer_text.length > max_length) {
answer.error_msg = "Answer longer than allowed"
break;
}
const patterns: PatternValidation[] = (question.validation ?? {})["patterns"] ?? []
patterns.forEach((pv) => {
let re = new RegExp(pv.pattern)
if (answer_text.search(re) == -1) {
answer.error_msg = pv.message
return;
}
})
break;
case "MC":
if (answer.values.includes(val)) {
// The item has already been selected
answer.values = answer.values.filter(value => value !== val);
} else {
// It's a new selection
if (question.selector == "SA") {
answer.values = [val]
} else if (question.selector == "MA") {
answer.values.push(val);
}
}
if (answer.values.length == 0) {
answer.error_msg = "MC question with no selected answers"
}
const choice_codes: string[] = question.choices?.map((c) => c.choice_id) ?? [];
switch (question.selector) {
case "SA":
if (answer.values.length > 1) {
answer.error_msg = "Single Answer MC question with >1 selected answers"
}
break;
case "MA":
if (answer.values.length > choice_codes.length) {
answer.error_msg = "More options selected than allowed"
}
break;
}
if (!answer.values.every(val => choice_codes.includes(val))) {
answer.error_msg = "Invalid Options Selected";
}
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"
*/
answer.error_msg = ""
break;
default:
throw new Error("Incorrect Question Type provided");
}
state[question.question_id] = answer
},
saveAnswer(state, action: PayloadAction<{ question: UpkQuestion }>) {
let question: UpkQuestion = action.payload.question;
let answer: Answer = state[question.question_id]
state[question.question_id] = {
'values': answer.values,
'error_msg': "",
'processing': false,
'complete': false
} as Answer
}
}
})
export const {addAnswer, saveAnswer} = answerSlice.actions;
export default answerSlice.reducer
export const answerForQuestion = (state: RootState, question: UpkQuestion) => state.answers[question.question_id] ?? {
values: [],
error_msg: "",
complete: false,
processing: false
} as Answer;
export const makeSelectChoicesByQuestion = (question: UpkQuestion) =>
createSelector(
(state: RootState) => state.answers,
(answers) => {
// const question = questions.find(q => q.id === questionId);
// return question?.choices ?? [];
return answers[question.question_id] ?? {
values: [],
error_msg: "",
complete: false,
processing: false
} as Answer;
}
);
|