Skip to content

Commit 5b0059a

Browse files
committed
MOBILE-2272 quiz: Support display options in questions
1 parent f144a29 commit 5b0059a

File tree

5 files changed

+179
-62
lines changed

5 files changed

+179
-62
lines changed

src/addon/mod/quiz/providers/quiz.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ export class AddonModQuizProvider {
215215
};
216216

217217
return site.read('mod_quiz_get_attempt_data', params, preSets);
218+
}).then((result) => {
219+
return this.parseQuestions(result);
218220
});
219221
}
220222

@@ -389,7 +391,9 @@ export class AddonModQuizProvider {
389391
...this.sitesProvider.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
390392
};
391393

392-
return site.read('mod_quiz_get_attempt_review', params, preSets);
394+
return site.read('mod_quiz_get_attempt_review', params, preSets).then((result) => {
395+
return this.parseQuestions(result);
396+
});
393397
});
394398
}
395399

@@ -427,6 +431,8 @@ export class AddonModQuizProvider {
427431

428432
return site.read('mod_quiz_get_attempt_summary', params, preSets).then((response) => {
429433
if (response && response.questions) {
434+
response = this.parseQuestions(response);
435+
430436
if (options.loadLocal) {
431437
return this.quizOfflineProvider.loadQuestionsLocalStates(attemptId, response.questions, site.getId());
432438
}
@@ -1560,6 +1566,27 @@ export class AddonModQuizProvider {
15601566
siteId);
15611567
}
15621568

1569+
/**
1570+
* Parse questions of a WS response.
1571+
*
1572+
* @param result Result to parse.
1573+
* @return Parsed result.
1574+
*/
1575+
parseQuestions(result: any): any {
1576+
for (let i = 0; i < result.questions.length; i++) {
1577+
const question = result.questions[i];
1578+
1579+
if (!question.displayoptions) {
1580+
// Site doesn't return displayoptions, stop.
1581+
break;
1582+
}
1583+
1584+
question.displayoptions = this.utils.objectToKeyValueMap(question.displayoptions, 'name', 'value');
1585+
}
1586+
1587+
return result;
1588+
}
1589+
15631590
/**
15641591
* Process an attempt, saving its data.
15651592
*

src/addon/qtype/calculated/providers/handler.ts

Lines changed: 89 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ import { AddonQtypeCalculatedComponent } from '../component/calculated';
2424
*/
2525
@Injectable()
2626
export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
27+
static UNITINPUT = '0';
28+
static UNITRADIO = '1';
29+
static UNITSELECT = '2';
30+
static UNITNONE = '3';
31+
32+
static UNITGRADED = '1';
33+
static UNITOPTIONAL = '0';
34+
2735
name = 'AddonQtypeCalculated';
2836
type = 'qtype_calculated';
2937

@@ -41,6 +49,23 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
4149
return AddonQtypeCalculatedComponent;
4250
}
4351

52+
/**
53+
* Check if the units are in a separate field for the question.
54+
*
55+
* @param question Question.
56+
* @return Whether units are in a separate field.
57+
*/
58+
hasSeparateUnitField(question: any): boolean {
59+
if (!question.displayoptions) {
60+
const element = this.domUtils.convertToElement(question.html);
61+
62+
return !!(element.querySelector('select[name*=unit]') || element.querySelector('input[type="radio"]'));
63+
}
64+
65+
return question.displayoptions.showunits === AddonQtypeCalculatedHandler.UNITRADIO ||
66+
question.displayoptions.showunits === AddonQtypeCalculatedHandler.UNITSELECT;
67+
}
68+
4469
/**
4570
* Check if a response is complete.
4671
*
@@ -51,15 +76,42 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
5176
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
5277
*/
5378
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
54-
if (this.isGradableResponse(question, answers) === 0 || !this.validateUnits(answers['answer'])) {
79+
if (!this.isGradableResponse(question, answers)) {
5580
return 0;
5681
}
5782

58-
if (this.requiresUnits(question)) {
59-
return this.isValidValue(answers['unit']) ? 1 : 0;
83+
const parsedAnswer = this.parseAnswer(question, answers['answer']);
84+
if (parsedAnswer.answer === null) {
85+
return 0;
86+
}
87+
88+
if (!question.displayoptions) {
89+
if (this.hasSeparateUnitField(question)) {
90+
return this.isValidValue(answers['unit']) ? 1 : 0;
91+
}
92+
93+
// We cannot know if the answer should contain units or not.
94+
return -1;
95+
}
96+
97+
if (question.displayoptions.showunits != AddonQtypeCalculatedHandler.UNITINPUT && parsedAnswer.unit) {
98+
// There should be no units or be outside of the input, not valid.
99+
return 0;
100+
}
101+
102+
if (this.hasSeparateUnitField(question) && !this.isValidValue(answers['unit'])) {
103+
// Unit not supplied as a separate field and it's required.
104+
return 0;
60105
}
61106

62-
return -1;
107+
if (question.displayoptions.showunits == AddonQtypeCalculatedHandler.UNITINPUT &&
108+
question.displayoptions.unitgradingtype == AddonQtypeCalculatedHandler.UNITGRADED &&
109+
!this.isValidValue(parsedAnswer.unit)) {
110+
// Unit not supplied inside the input and it's required.
111+
return 0;
112+
}
113+
114+
return 1;
63115
}
64116

65117
/**
@@ -80,13 +132,7 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
80132
* @return 1 if gradable, 0 if not gradable, -1 if cannot determine.
81133
*/
82134
isGradableResponse(question: any, answers: any): number {
83-
let isGradable = this.isValidValue(answers['answer']);
84-
if (isGradable && this.requiresUnits(question)) {
85-
// The question requires a unit.
86-
isGradable = this.isValidValue(answers['unit']);
87-
}
88-
89-
return isGradable ? 1 : 0;
135+
return this.isValidValue(answers['answer']) ? 1 : 0;
90136
}
91137

92138
/**
@@ -115,48 +161,56 @@ export class AddonQtypeCalculatedHandler implements CoreQuestionHandler {
115161
}
116162

117163
/**
118-
* Check if a question requires units in a separate input.
119-
*
120-
* @param question The question.
121-
* @return Whether the question requires units.
122-
*/
123-
requiresUnits(question: any): boolean {
124-
const element = this.domUtils.convertToElement(question.html);
125-
126-
return !!(element.querySelector('select[name*=unit]') || element.querySelector('input[type="radio"]'));
127-
}
128-
129-
/**
130-
* Validate a number with units. We don't have the list of valid units and conversions, so we can't perform
131-
* a full validation. If this function returns true it means we can't be sure it's valid.
164+
* Parse an answer string.
132165
*
166+
* @param question Question.
133167
* @param answer Answer.
134-
* @return False if answer isn't valid, true if we aren't sure if it's valid.
168+
* @return 0 if answer isn't valid, 1 if answer is valid, -1 if we aren't sure if it's valid.
135169
*/
136-
validateUnits(answer: string): boolean {
170+
parseAnswer(question: any, answer: string): {answer: number, unit: string} {
137171
if (!answer) {
138-
return false;
172+
return {answer: null, unit: null};
139173
}
140174

141-
const regexString = '[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[-+]?\\d+)?';
175+
let regexString = '[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[-+]?\\d+)?';
142176

143177
// Strip spaces (which may be thousands separators) and change other forms of writing e to e.
144-
answer = answer.replace(' ', '');
178+
answer = answer.replace(/ /g, '');
145179
answer = answer.replace(/(?:e|E|(?:x|\*|×)10(?:\^|\*\*))([+-]?\d+)/, 'e$1');
146180

147-
// If a '.' is present or there are multiple ',' (i.e. 2,456,789) assume ',' is a thousands separator and stip it.
181+
// If a '.' is present or there are multiple ',' (i.e. 2,456,789) assume ',' is a thousands separator and strip it.
148182
// Else assume it is a decimal separator, and change it to '.'.
149183
if (answer.indexOf('.') != -1 || answer.split(',').length - 1 > 1) {
150184
answer = answer.replace(',', '');
151185
} else {
152186
answer = answer.replace(',', '.');
153187
}
154188

155-
// We don't know if units should be before or after so we check both.
156-
if (answer.match(new RegExp('^' + regexString)) === null || answer.match(new RegExp(regexString + '$')) === null) {
157-
return false;
189+
let unitsLeft = false;
190+
let match = null;
191+
192+
if (!question.displayoptions) {
193+
// We don't know if units should be before or after so we check both.
194+
match = answer.match(new RegExp('^' + regexString));
195+
if (!match) {
196+
unitsLeft = true;
197+
match = answer.match(new RegExp(regexString + '$'));
198+
}
199+
} else {
200+
unitsLeft = question.displayoptions.unitsleft == '1';
201+
regexString = unitsLeft ? regexString + '$' : '^' + regexString;
202+
203+
match = answer.match(new RegExp(regexString));
204+
}
205+
206+
if (!match) {
207+
return {answer: null, unit: null};
158208
}
159209

160-
return true;
210+
const numberString = match[0];
211+
const unit = unitsLeft ? answer.substr(0, answer.length - match[0].length) : answer.substr(match[0].length);
212+
213+
// No need to calculate the multiplier.
214+
return {answer: Number(numberString), unit: unit};
161215
}
162216
}

src/addon/qtype/essay/component/addon-qtype-essay.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
<!-- Attachments. -->
2929
<ng-container *ngIf="question.allowsAttachments">
30-
<core-attachments *ngIf="uploadFilesSupported" [files]="attachments" [component]="component" [componentId]="componentId" [maxSize]="question.attachmentsMaxBytes" [maxSubmissions]="question.attachmentsMaxFiles" [allowOffline]="offlineEnabled"></core-attachments>
30+
<core-attachments *ngIf="uploadFilesSupported" [files]="attachments" [component]="component" [componentId]="componentId" [maxSize]="question.attachmentsMaxBytes" [maxSubmissions]="question.attachmentsMaxFiles" [allowOffline]="offlineEnabled" [acceptedTypes]="question.attachmentsAcceptedTypes"></core-attachments>
3131

3232
<input item-content *ngIf="uploadFilesSupported" type="hidden" [name]="question.attachmentsDraftIdInput.name" [value]="question.attachmentsDraftIdInput.value" >
3333

src/addon/qtype/essay/providers/handler.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,28 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
6767
return this.questionHelper.deleteStoredQuestionFiles(question, component, componentId, siteId);
6868
}
6969

70+
/**
71+
* Check whether the question allows text and/or attachments.
72+
*
73+
* @param question Question to check.
74+
* @return Allowed options.
75+
*/
76+
protected getAllowedOptions(question: any): {text: boolean, attachments: boolean} {
77+
if (question.displayoptions) {
78+
return {
79+
text: question.displayoptions.responseformat != 'noinline',
80+
attachments: question.displayoptions.attachments != '0',
81+
};
82+
} else {
83+
const element = this.domUtils.convertToElement(question.html);
84+
85+
return {
86+
text: !!element.querySelector('textarea[name*=_answer]'),
87+
attachments: !!element.querySelector('div[id*=filemanager]'),
88+
};
89+
}
90+
}
91+
7092
/**
7193
* Return the name of the behaviour to use for the question.
7294
* If the question should use the default behaviour you shouldn't implement this function.
@@ -122,15 +144,13 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
122144
* @return 1 if complete, 0 if not complete, -1 if cannot determine.
123145
*/
124146
isCompleteResponse(question: any, answers: any, component: string, componentId: string | number): number {
125-
const element = this.domUtils.convertToElement(question.html);
126147

127-
const hasInlineText = answers['answer'] && answers['answer'] !== '';
128-
const allowsInlineText = !!element.querySelector('textarea[name*=_answer]');
129-
const allowsAttachments = !!element.querySelector('div[id*=filemanager]');
148+
const hasTextAnswer = answers['answer'] && answers['answer'] !== '';
130149
const uploadFilesSupported = typeof question.responsefileareas != 'undefined';
150+
const allowedOptions = this.getAllowedOptions(question);
131151

132-
if (!allowsAttachments) {
133-
return hasInlineText ? 1 : 0;
152+
if (!allowedOptions.attachments) {
153+
return hasTextAnswer ? 1 : 0;
134154
}
135155

136156
if (!uploadFilesSupported) {
@@ -141,12 +161,12 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
141161
const questionComponentId = CoreQuestion.instance.getQuestionComponentId(question, componentId);
142162
const attachments = CoreFileSession.instance.getFiles(component, questionComponentId);
143163

144-
if (!allowsInlineText) {
164+
if (!allowedOptions.text) {
145165
return attachments && attachments.length > 0 ? 1 : 0;
146166
}
147167

148-
// If any of the fields is missing return -1 because we can't know if they're required or not.
149-
return hasInlineText && attachments && attachments.length > 0 ? 1 : -1;
168+
return (hasTextAnswer || question.displayoptions.responserequired == '0') &&
169+
((attachments && attachments.length > 0) || question.displayoptions.attachmentsrequired == '0') ? 1 : 0;
150170
}
151171

152172
/**
@@ -181,15 +201,13 @@ export class AddonQtypeEssayHandler implements CoreQuestionHandler {
181201
* @return Whether they're the same.
182202
*/
183203
isSameResponse(question: any, prevAnswers: any, newAnswers: any, component: string, componentId: string | number): boolean {
184-
const element = this.domUtils.convertToElement(question.html);
185-
const allowsInlineText = !!element.querySelector('textarea[name*=_answer]');
186-
const allowsAttachments = !!element.querySelector('div[id*=filemanager]');
187204
const uploadFilesSupported = typeof question.responsefileareas != 'undefined';
205+
const allowedOptions = this.getAllowedOptions(question);
188206

189207
// First check the inline text.
190-
const answerIsEqual = allowsInlineText ? this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer') : true;
208+
const answerIsEqual = allowedOptions.text ? this.utils.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer') : true;
191209

192-
if (!allowsAttachments || !uploadFilesSupported || !answerIsEqual) {
210+
if (!allowedOptions.attachments || !uploadFilesSupported || !answerIsEqual) {
193211
// No need to check attachments.
194212
return answerIsEqual;
195213
}

0 commit comments

Comments
 (0)