|
12 | 12 | // See the License for the specific language governing permissions and |
13 | 13 | // limitations under the License. |
14 | 14 |
|
15 | | -import { Injectable, Type } from '@angular/core'; |
| 15 | +import { Injectable } from '@angular/core'; |
| 16 | +import { AddonQtypeNumericalHandlerService } from '@addons/qtype/numerical/services/handlers/numerical'; |
16 | 17 |
|
17 | | -import { CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question'; |
18 | | -import { CoreQuestionHandler } from '@features/question/services/question-delegate'; |
19 | | -import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; |
20 | | -import { CoreObject } from '@singletons/object'; |
21 | | -import { makeSingleton, Translate } from '@singletons'; |
| 18 | +import { makeSingleton } from '@singletons'; |
22 | 19 |
|
23 | 20 | /** |
24 | 21 | * Handler to support calculated question type. |
| 22 | + * This question type depends on numeric question type. |
25 | 23 | */ |
26 | 24 | @Injectable({ providedIn: 'root' }) |
27 | | -export class AddonQtypeCalculatedHandlerService implements CoreQuestionHandler { |
28 | | - |
29 | | - static readonly UNITINPUT = '0'; |
30 | | - static readonly UNITRADIO = '1'; |
31 | | - static readonly UNITSELECT = '2'; |
32 | | - static readonly UNITNONE = '3'; |
33 | | - |
34 | | - static readonly UNITGRADED = '1'; |
35 | | - static readonly UNITOPTIONAL = '0'; |
| 25 | +export class AddonQtypeCalculatedHandlerService extends AddonQtypeNumericalHandlerService { |
36 | 26 |
|
37 | 27 | name = 'AddonQtypeCalculated'; |
38 | 28 | type = 'qtype_calculated'; |
39 | 29 |
|
40 | | - /** |
41 | | - * @inheritdoc |
42 | | - */ |
43 | | - async getComponent(): Promise<Type<unknown>> { |
44 | | - const { AddonQtypeCalculatedComponent } = await import('../../component/calculated'); |
45 | | - |
46 | | - return AddonQtypeCalculatedComponent; |
47 | | - } |
48 | | - |
49 | | - /** |
50 | | - * Check if the units are in a separate field for the question. |
51 | | - * |
52 | | - * @param question Question. |
53 | | - * @returns Whether units are in a separate field. |
54 | | - */ |
55 | | - hasSeparateUnitField(question: CoreQuestionQuestionParsed): boolean { |
56 | | - if (!question.parsedSettings) { |
57 | | - const element = convertTextToHTMLElement(question.html); |
58 | | - |
59 | | - return !!(element.querySelector('select[name*=unit]') || element.querySelector('input[type="radio"]')); |
60 | | - } |
61 | | - |
62 | | - return question.parsedSettings.unitdisplay === AddonQtypeCalculatedHandlerService.UNITRADIO || |
63 | | - question.parsedSettings.unitdisplay === AddonQtypeCalculatedHandlerService.UNITSELECT; |
64 | | - } |
65 | | - |
66 | | - /** |
67 | | - * @inheritdoc |
68 | | - */ |
69 | | - isCompleteResponse( |
70 | | - question: CoreQuestionQuestionParsed, |
71 | | - answers: CoreQuestionsAnswers, |
72 | | - ): number { |
73 | | - if (!this.isGradableResponse(question, answers)) { |
74 | | - return 0; |
75 | | - } |
76 | | - |
77 | | - const { answer, unit } = this.parseAnswer(question, <string> answers.answer); |
78 | | - if (answer === null) { |
79 | | - return 0; |
80 | | - } |
81 | | - |
82 | | - if (!question.parsedSettings) { |
83 | | - if (this.hasSeparateUnitField(question)) { |
84 | | - return this.isValidValue(<string> answers.unit) ? 1 : 0; |
85 | | - } |
86 | | - |
87 | | - // We cannot know if the answer should contain units or not. |
88 | | - return -1; |
89 | | - } |
90 | | - |
91 | | - if (question.parsedSettings.unitdisplay !== AddonQtypeCalculatedHandlerService.UNITINPUT && unit) { |
92 | | - // There should be no units or be outside of the input, not valid. |
93 | | - return 0; |
94 | | - } |
95 | | - |
96 | | - if (this.hasSeparateUnitField(question) && !this.isValidValue(<string> answers.unit)) { |
97 | | - // Unit not supplied as a separate field and it's required. |
98 | | - return 0; |
99 | | - } |
100 | | - |
101 | | - if (question.parsedSettings.unitdisplay === AddonQtypeCalculatedHandlerService.UNITINPUT && |
102 | | - question.parsedSettings.unitgradingtype === AddonQtypeCalculatedHandlerService.UNITGRADED && |
103 | | - !this.isValidValue(unit)) { |
104 | | - // Unit not supplied inside the input and it's required. |
105 | | - return 0; |
106 | | - } |
107 | | - |
108 | | - return 1; |
109 | | - } |
110 | | - |
111 | | - /** |
112 | | - * @inheritdoc |
113 | | - */ |
114 | | - async isEnabled(): Promise<boolean> { |
115 | | - return true; |
116 | | - } |
117 | | - |
118 | | - /** |
119 | | - * @inheritdoc |
120 | | - */ |
121 | | - isGradableResponse( |
122 | | - question: CoreQuestionQuestionParsed, |
123 | | - answers: CoreQuestionsAnswers, |
124 | | - ): number { |
125 | | - return this.isValidValue(<string> answers.answer) ? 1 : 0; |
126 | | - } |
127 | | - |
128 | | - /** |
129 | | - * @inheritdoc |
130 | | - */ |
131 | | - isSameResponse( |
132 | | - question: CoreQuestionQuestionParsed, |
133 | | - prevAnswers: CoreQuestionsAnswers, |
134 | | - newAnswers: CoreQuestionsAnswers, |
135 | | - ): boolean { |
136 | | - return CoreObject.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer') && |
137 | | - CoreObject.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'unit'); |
138 | | - } |
139 | | - |
140 | | - /** |
141 | | - * Check if a value is valid (not empty). |
142 | | - * |
143 | | - * @param value Value to check. |
144 | | - * @returns Whether the value is valid. |
145 | | - */ |
146 | | - isValidValue(value: string | number | null): boolean { |
147 | | - return !!value || value === '0' || value === 0; |
148 | | - } |
149 | | - |
150 | | - /** |
151 | | - * Parse an answer string. |
152 | | - * |
153 | | - * @param question Question. |
154 | | - * @param answer Answer. |
155 | | - * @returns Answer and unit. |
156 | | - */ |
157 | | - parseAnswer(question: CoreQuestionQuestionParsed, answer: string): { answer: number | null; unit: string | null } { |
158 | | - if (!answer) { |
159 | | - return { answer: null, unit: null }; |
160 | | - } |
161 | | - |
162 | | - let regexString = '[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[-+]?\\d+)?'; |
163 | | - |
164 | | - // Strip spaces (which may be thousands separators) and change other forms of writing e to e. |
165 | | - answer = answer.replace(/ /g, ''); |
166 | | - answer = answer.replace(/(?:e|E|(?:x|\*|×)10(?:\^|\*\*))([+-]?\d+)/, 'e$1'); |
167 | | - |
168 | | - // If a '.' is present or there are multiple ',' (i.e. 2,456,789) assume ',' is a thousands separator and strip it. |
169 | | - // Else assume it is a decimal separator, and change it to '.'. |
170 | | - if (answer.indexOf('.') !== -1 || answer.split(',').length - 1 > 1) { |
171 | | - answer = answer.replace(',', ''); |
172 | | - } else { |
173 | | - answer = answer.replace(',', '.'); |
174 | | - } |
175 | | - |
176 | | - let unitsLeft = false; |
177 | | - let match: RegExpMatchArray | null = null; |
178 | | - |
179 | | - if (!question.parsedSettings || question.parsedSettings.unitsleft === null) { |
180 | | - // We don't know if units should be before or after so we check both. |
181 | | - match = answer.match(new RegExp('^' + regexString)); |
182 | | - if (!match) { |
183 | | - unitsLeft = true; |
184 | | - match = answer.match(new RegExp(regexString + '$')); |
185 | | - } |
186 | | - } else { |
187 | | - unitsLeft = question.parsedSettings.unitsleft === '1'; |
188 | | - regexString = unitsLeft ? regexString + '$' : '^' + regexString; |
189 | | - |
190 | | - match = answer.match(new RegExp(regexString)); |
191 | | - } |
192 | | - |
193 | | - if (!match) { |
194 | | - return { answer: null, unit: null }; |
195 | | - } |
196 | | - |
197 | | - const numberString = match[0]; |
198 | | - const unit = unitsLeft ? answer.substring(0, answer.length - match[0].length) : answer.substring(match[0].length); |
199 | | - |
200 | | - // No need to calculate the multiplier. |
201 | | - return { answer: Number(numberString), unit }; |
202 | | - } |
203 | | - |
204 | | - /** |
205 | | - * @inheritdoc |
206 | | - */ |
207 | | - getValidationError( |
208 | | - question: CoreQuestionQuestionParsed, |
209 | | - answers: CoreQuestionsAnswers, |
210 | | - ): string | undefined { |
211 | | - if (!this.isGradableResponse(question, answers)) { |
212 | | - return Translate.instant('addon.qtype_numerical.pleaseenterananswer'); |
213 | | - } |
214 | | - |
215 | | - const { answer, unit } = this.parseAnswer(question, <string> answers.answer); |
216 | | - if (answer === null) { |
217 | | - return Translate.instant('addon.qtype_numerica.invalidnumber'); |
218 | | - } |
219 | | - |
220 | | - if (!question.parsedSettings) { |
221 | | - if (this.hasSeparateUnitField(question)) { |
222 | | - return Translate.instant('addon.qtype_numerica.unitnotselected'); |
223 | | - } |
224 | | - |
225 | | - // We cannot know if the answer should contain units or not. |
226 | | - return; |
227 | | - } |
228 | | - |
229 | | - if (question.parsedSettings.unitdisplay !== AddonQtypeCalculatedHandlerService.UNITINPUT && unit) { |
230 | | - return Translate.instant('addon.qtype_numerica.invalidnumbernounit'); |
231 | | - } |
232 | | - |
233 | | - if (question.parsedSettings.unitdisplay === AddonQtypeCalculatedHandlerService.UNITINPUT && |
234 | | - question.parsedSettings.unitgradingtype === AddonQtypeCalculatedHandlerService.UNITGRADED && |
235 | | - !this.isValidValue(unit)) { |
236 | | - return Translate.instant('addon.qtype_numerica.invalidnumber'); |
237 | | - } |
238 | | - |
239 | | - return; |
240 | | - } |
241 | | - |
242 | 30 | } |
243 | 31 |
|
244 | 32 | export const AddonQtypeCalculatedHandler = makeSingleton(AddonQtypeCalculatedHandlerService); |
0 commit comments