Skip to content

Commit 490847f

Browse files
committed
MOBILE-4690 quiz: Calculated depends on numerical like in LMS
1 parent 71e05cc commit 490847f

File tree

11 files changed

+248
-312
lines changed

11 files changed

+248
-312
lines changed

src/addons/qtype/calculated/calculated.module.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,10 @@
1414

1515
import { APP_INITIALIZER, NgModule } from '@angular/core';
1616

17-
import { CoreSharedModule } from '@/core/shared.module';
1817
import { CoreQuestionDelegate } from '@features/question/services/question-delegate';
19-
import { AddonQtypeCalculatedComponent } from './component/calculated';
2018
import { AddonQtypeCalculatedHandler } from './services/handlers/calculated';
2119

2220
@NgModule({
23-
declarations: [
24-
AddonQtypeCalculatedComponent,
25-
],
26-
imports: [
27-
CoreSharedModule,
28-
],
2921
providers: [
3022
{
3123
provide: APP_INITIALIZER,
@@ -35,8 +27,5 @@ import { AddonQtypeCalculatedHandler } from './services/handlers/calculated';
3527
},
3628
},
3729
],
38-
exports: [
39-
AddonQtypeCalculatedComponent,
40-
],
4130
})
4231
export class AddonQtypeCalculatedModule {}

src/addons/qtype/calculated/services/handlers/calculated.ts

Lines changed: 5 additions & 217 deletions
Original file line numberDiff line numberDiff line change
@@ -12,233 +12,21 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import { Injectable, Type } from '@angular/core';
15+
import { Injectable } from '@angular/core';
16+
import { AddonQtypeNumericalHandlerService } from '@addons/qtype/numerical/services/handlers/numerical';
1617

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';
2219

2320
/**
2421
* Handler to support calculated question type.
22+
* This question type depends on numeric question type.
2523
*/
2624
@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 {
3626

3727
name = 'AddonQtypeCalculated';
3828
type = 'qtype_calculated';
3929

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-
24230
}
24331

24432
export const AddonQtypeCalculatedHandler = makeSingleton(AddonQtypeCalculatedHandlerService);

src/addons/qtype/calculatedsimple/services/handlers/calculatedsimple.ts

Lines changed: 4 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -12,83 +12,21 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import { Injectable, Type } from '@angular/core';
15+
import { Injectable } from '@angular/core';
1616

17-
import { CoreQuestionHandler } from '@features/question/services/question-delegate';
18-
import { AddonQtypeCalculatedHandler } from '@addons/qtype/calculated/services/handlers/calculated';
19-
import { CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question';
2017
import { makeSingleton } from '@singletons';
18+
import { AddonQtypeNumericalHandlerService } from '@addons/qtype/numerical/services/handlers/numerical';
2119

2220
/**
2321
* Handler to support calculated simple question type.
22+
* This question type depends on numeric question type.
2423
*/
2524
@Injectable({ providedIn: 'root' })
26-
export class AddonQtypeCalculatedSimpleHandlerService implements CoreQuestionHandler {
25+
export class AddonQtypeCalculatedSimpleHandlerService extends AddonQtypeNumericalHandlerService {
2726

2827
name = 'AddonQtypeCalculatedSimple';
2928
type = 'qtype_calculatedsimple';
3029

31-
/**
32-
* @inheritdoc
33-
*/
34-
async getComponent(): Promise<Type<unknown>> {
35-
// Calculated simple behaves like a calculated, use the same component.
36-
const { AddonQtypeCalculatedComponent } = await import('@addons/qtype/calculated/component/calculated');
37-
38-
return AddonQtypeCalculatedComponent;
39-
}
40-
41-
/**
42-
* @inheritdoc
43-
*/
44-
isCompleteResponse(
45-
question: CoreQuestionQuestionParsed,
46-
answers: CoreQuestionsAnswers,
47-
): number {
48-
// This question type depends on calculated.
49-
return AddonQtypeCalculatedHandler.isCompleteResponse(question, answers);
50-
}
51-
52-
/**
53-
* @inheritdoc
54-
*/
55-
async isEnabled(): Promise<boolean> {
56-
return true;
57-
}
58-
59-
/**
60-
* @inheritdoc
61-
*/
62-
isGradableResponse(
63-
question: CoreQuestionQuestionParsed,
64-
answers: CoreQuestionsAnswers,
65-
): number {
66-
// This question type depends on calculated.
67-
return AddonQtypeCalculatedHandler.isGradableResponse(question, answers);
68-
}
69-
70-
/**
71-
* @inheritdoc
72-
*/
73-
isSameResponse(
74-
question: CoreQuestionQuestionParsed,
75-
prevAnswers: CoreQuestionsAnswers,
76-
newAnswers: CoreQuestionsAnswers,
77-
): boolean {
78-
// This question type depends on calculated.
79-
return AddonQtypeCalculatedHandler.isSameResponse(question, prevAnswers, newAnswers);
80-
}
81-
82-
/**
83-
* @inheritdoc
84-
*/
85-
getValidationError(
86-
question: CoreQuestionQuestionParsed,
87-
answers: CoreQuestionsAnswers,
88-
): string | undefined {
89-
return AddonQtypeCalculatedHandler.getValidationError(question, answers);
90-
}
91-
9230
}
9331

9432
export const AddonQtypeCalculatedSimpleHandler = makeSingleton(AddonQtypeCalculatedSimpleHandlerService);

src/addons/qtype/ddwtos/services/handlers/ddwtos.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { makeSingleton } from '@singletons';
1919

2020
/**
2121
* Handler to support drag-and-drop words into sentences question type.
22+
* This question type is a variation of gapselect.
2223
*/
2324
@Injectable({ providedIn: 'root' })
2425
export class AddonQtypeDdwtosHandlerService extends AddonQtypeGapSelectHandlerService {

src/addons/qtype/calculated/component/addon-qtype-calculated.html renamed to src/addons/qtype/numerical/component/numerical.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<ion-list class="addon-qtype-calculated-container" *ngIf="question && (question.text || question.text === '')">
1+
<ion-list class="addon-qtype-numerical-container" *ngIf="question && (question.text || question.text === '')">
22
<ion-item class="ion-text-wrap">
33
<ion-label>
44
<core-format-text [component]="component" [componentId]="componentId" [text]="question.text" [contextLevel]="contextLevel"
File renamed without changes.

src/addons/qtype/calculated/component/calculated.ts renamed to src/addons/qtype/numerical/component/numerical.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,20 @@
1414

1515
import { Component, ElementRef } from '@angular/core';
1616

17-
import { AddonModQuizCalculatedQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
17+
import { AddonModQuizNumericalQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
1818

1919
/**
20-
* Component to render a calculated question.
20+
* Component to render a numerical question.
2121
*/
2222
@Component({
23-
selector: 'addon-qtype-calculated',
24-
templateUrl: 'addon-qtype-calculated.html',
25-
styleUrl: 'calculated.scss',
23+
selector: 'numerical',
24+
templateUrl: 'numerical.html',
25+
styleUrl: 'numerical.scss',
2626
})
27-
export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent<AddonModQuizCalculatedQuestion> {
27+
export class AddonQtypeNumericalComponent extends CoreQuestionBaseComponent<AddonModQuizNumericalQuestion> {
2828

2929
constructor(elementRef: ElementRef) {
30-
super('AddonQtypeCalculatedComponent', elementRef);
30+
super('AddonQtypeNumericalComponent', elementRef);
3131
}
3232

3333
/**

0 commit comments

Comments
 (0)