Skip to content

Commit

Permalink
MOBILE-4690 quiz: Add validation errors for all question types
Browse files Browse the repository at this point in the history
  • Loading branch information
crazyserver committed Jan 16, 2025
1 parent 3980444 commit 59d38dc
Show file tree
Hide file tree
Showing 14 changed files with 207 additions and 90 deletions.
50 changes: 44 additions & 6 deletions src/addons/qtype/calculated/services/handlers/calculated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/ques
import { CoreQuestionHandler } from '@features/question/services/question-delegate';
import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
import { CoreObject } from '@singletons/object';
import { makeSingleton } from '@singletons';
import { makeSingleton, Translate } from '@singletons';

/**
* Handler to support calculated question type.
Expand Down Expand Up @@ -88,7 +88,7 @@ export class AddonQtypeCalculatedHandlerService implements CoreQuestionHandler {
return -1;
}

if (question.parsedSettings.unitdisplay != AddonQtypeCalculatedHandlerService.UNITINPUT && unit) {
if (question.parsedSettings.unitdisplay !== AddonQtypeCalculatedHandlerService.UNITINPUT && unit) {
// There should be no units or be outside of the input, not valid.
return 0;
}
Expand All @@ -98,8 +98,8 @@ export class AddonQtypeCalculatedHandlerService implements CoreQuestionHandler {
return 0;
}

if (question.parsedSettings.unitdisplay == AddonQtypeCalculatedHandlerService.UNITINPUT &&
question.parsedSettings.unitgradingtype == AddonQtypeCalculatedHandlerService.UNITGRADED &&
if (question.parsedSettings.unitdisplay === AddonQtypeCalculatedHandlerService.UNITINPUT &&
question.parsedSettings.unitgradingtype === AddonQtypeCalculatedHandlerService.UNITGRADED &&
!this.isValidValue(unit)) {
// Unit not supplied inside the input and it's required.
return 0;
Expand Down Expand Up @@ -167,7 +167,7 @@ export class AddonQtypeCalculatedHandlerService implements CoreQuestionHandler {

// If a '.' is present or there are multiple ',' (i.e. 2,456,789) assume ',' is a thousands separator and strip it.
// Else assume it is a decimal separator, and change it to '.'.
if (answer.indexOf('.') != -1 || answer.split(',').length - 1 > 1) {
if (answer.indexOf('.') !== -1 || answer.split(',').length - 1 > 1) {
answer = answer.replace(',', '');
} else {
answer = answer.replace(',', '.');
Expand All @@ -184,7 +184,7 @@ export class AddonQtypeCalculatedHandlerService implements CoreQuestionHandler {
match = answer.match(new RegExp(regexString + '$'));
}
} else {
unitsLeft = question.parsedSettings.unitsleft == '1';
unitsLeft = question.parsedSettings.unitsleft === '1';
regexString = unitsLeft ? regexString + '$' : '^' + regexString;

match = answer.match(new RegExp(regexString));
Expand All @@ -201,6 +201,44 @@ export class AddonQtypeCalculatedHandlerService implements CoreQuestionHandler {
return { answer: Number(numberString), unit };
}

/**
* @inheritdoc
*/
getValidationError(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
): string | undefined {
if (!this.isGradableResponse(question, answers)) {
return Translate.instant('addon.qtype_numerical.pleaseenterananswer');
}

const { answer, unit } = this.parseAnswer(question, <string> answers.answer);
if (answer === null) {
return Translate.instant('addon.qtype_numerica.invalidnumber');
}

if (!question.parsedSettings) {
if (this.hasSeparateUnitField(question)) {
return Translate.instant('addon.qtype_numerica.unitnotselected');
}

// We cannot know if the answer should contain units or not.
return;
}

if (question.parsedSettings.unitdisplay !== AddonQtypeCalculatedHandlerService.UNITINPUT && unit) {
return Translate.instant('addon.qtype_numerica.invalidnumbernounit');
}

if (question.parsedSettings.unitdisplay === AddonQtypeCalculatedHandlerService.UNITINPUT &&
question.parsedSettings.unitgradingtype === AddonQtypeCalculatedHandlerService.UNITGRADED &&
!this.isValidValue(unit)) {
return Translate.instant('addon.qtype_numerica.pleaseenteranswerwithoutthousandssep');
}

return;
}

}

export const AddonQtypeCalculatedHandler = makeSingleton(AddonQtypeCalculatedHandlerService);
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ export class AddonQtypeCalculatedMultiHandlerService implements CoreQuestionHand
return AddonQtypeMultichoiceHandler.isSameResponseSingle(prevAnswers, newAnswers);
}

/**
* @inheritdoc
*/
getValidationError(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
): string | undefined {
return AddonQtypeMultichoiceHandler.getValidationError(question, answers);
}

}

export const AddonQtypeCalculatedMultiHandler = makeSingleton(AddonQtypeCalculatedMultiHandlerService);
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ export class AddonQtypeCalculatedSimpleHandlerService implements CoreQuestionHan
return AddonQtypeCalculatedHandler.isSameResponse(question, prevAnswers, newAnswers);
}

/**
* @inheritdoc
*/
getValidationError(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
): string | undefined {
return AddonQtypeCalculatedHandler.getValidationError(question, answers);
}

}

export const AddonQtypeCalculatedSimpleHandler = makeSingleton(AddonQtypeCalculatedSimpleHandlerService);
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Injectable, Type } from '@angular/core';

import { CoreQuestion, CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question';
import { CoreQuestionHandler } from '@features/question/services/question-delegate';
import { makeSingleton } from '@singletons';
import { makeSingleton, Translate } from '@singletons';

/**
* Handler to support drag-and-drop onto image question type.
Expand Down Expand Up @@ -101,6 +101,20 @@ export class AddonQtypeDdImageOrTextHandlerService implements CoreQuestionHandle
return CoreQuestion.compareAllAnswers(prevAnswers, newAnswers);
}

/**
* @inheritdoc
*/
getValidationError(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
): string | undefined {
if (this.isCompleteResponse(question, answers)) {
return;
}

return Translate.instant('addon.qtype_ddimageortext.pleasedraganimagetoeachdropregion');
}

}

export const AddonQtypeDdImageOrTextHandler = makeSingleton(AddonQtypeDdImageOrTextHandlerService);
18 changes: 16 additions & 2 deletions src/addons/qtype/ddmarker/services/handlers/ddmarker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { CoreQuestion, CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '
import { CoreQuestionHandler } from '@features/question/services/question-delegate';
import { CoreQuestionHelper, CoreQuestionQuestion } from '@features/question/services/question-helper';
import { CoreWSFile } from '@services/ws';
import { makeSingleton } from '@singletons';
import { makeSingleton, Translate } from '@singletons';

/**
* Handler to support drag-and-drop markers question type.
Expand Down Expand Up @@ -102,7 +102,7 @@ export class AddonQtypeDdMarkerHandlerService implements CoreQuestionHandler {

CoreQuestionHelper.extractQuestionScripts(treatedQuestion, usageId);

if (treatedQuestion.amdArgs && typeof treatedQuestion.amdArgs[1] == 'string') {
if (treatedQuestion.amdArgs && typeof treatedQuestion.amdArgs[1] === 'string') {
// Moodle 3.6+.
return [{
fileurl: treatedQuestion.amdArgs[1],
Expand All @@ -112,6 +112,20 @@ export class AddonQtypeDdMarkerHandlerService implements CoreQuestionHandler {
return [];
}

/**
* @inheritdoc
*/
getValidationError(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
): string | undefined {
if (this.isCompleteResponse(question, answers)) {
return;
}

return Translate.instant('addon.qtype_ddmarker.pleasedragatleastonemarker');
}

}

export const AddonQtypeDdMarkerHandler = makeSingleton(AddonQtypeDdMarkerHandlerService);
68 changes: 2 additions & 66 deletions src/addons/qtype/ddwtos/services/handlers/ddwtos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { AddonQtypeGapSelectHandlerService } from '@addons/qtype/gapselect/services/handlers/gapselect';
import { Injectable, Type } from '@angular/core';

import { CoreQuestion, CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question';
import { CoreQuestionHandler } from '@features/question/services/question-delegate';
import { makeSingleton } from '@singletons';

/**
* Handler to support drag-and-drop words into sentences question type.
*/
@Injectable({ providedIn: 'root' })
export class AddonQtypeDdwtosHandlerService implements CoreQuestionHandler {
export class AddonQtypeDdwtosHandlerService extends AddonQtypeGapSelectHandlerService {

name = 'AddonQtypeDdwtos';
type = 'qtype_ddwtos';

/**
* @inheritdoc
*/
getBehaviour(question: CoreQuestionQuestionParsed, behaviour: string): string {
if (behaviour === 'interactive') {
return 'interactivecountback';
}

return behaviour;
}

/**
* @inheritdoc
*/
Expand All @@ -47,58 +35,6 @@ export class AddonQtypeDdwtosHandlerService implements CoreQuestionHandler {
return AddonQtypeDdwtosComponent;
}

/**
* @inheritdoc
*/
isCompleteResponse(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
): number {
for (const name in answers) {
const value = answers[name];
if (!value || value === '0') {
return 0;
}
}

return 1;
}

/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}

/**
* @inheritdoc
*/
isGradableResponse(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
): number {
for (const name in answers) {
const value = answers[name];
if (value && value !== '0') {
return 1;
}
}

return 0;
}

/**
* @inheritdoc
*/
isSameResponse(
question: CoreQuestionQuestionParsed,
prevAnswers: CoreQuestionsAnswers,
newAnswers: CoreQuestionsAnswers,
): boolean {
return CoreQuestion.compareAllAnswers(prevAnswers, newAnswers);
}

}

export const AddonQtypeDdwtosHandler = makeSingleton(AddonQtypeDdwtosHandlerService);
16 changes: 11 additions & 5 deletions src/addons/qtype/essay/services/handlers/essay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
protected getAllowedOptions(question: CoreQuestionQuestionParsed): { text: boolean; attachments: boolean } {
if (question.parsedSettings) {
return {
text: question.parsedSettings.responseformat != 'noinline',
attachments: question.parsedSettings.attachments != '0',
text: question.parsedSettings.responseformat !== 'noinline',
attachments: question.parsedSettings.attachments !== '0',
};
}

Expand Down Expand Up @@ -137,6 +137,8 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
onlineError: string | undefined,
component: string,
componentId: string | number,
): string | undefined {
if (answers.answer === undefined) {
// Not answered in offline.
Expand All @@ -148,6 +150,10 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
return;
}

if (this.isCompleteResponse(question, answers, component, componentId) === 0) {
return;
}

return this.checkInputWordCount(question, <string> answers.answer, onlineError);
}

Expand Down Expand Up @@ -221,7 +227,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
return attachments && attachments.length >= Number(question.parsedSettings.attachmentsrequired) ? 1 : 0;
}

return ((hasTextAnswer || question.parsedSettings.responserequired == '0') &&
return ((hasTextAnswer || question.parsedSettings.responserequired === '0') &&
(attachments && attachments.length >= Number(question.parsedSettings.attachmentsrequired))) ? 1 : 0;
}

Expand Down Expand Up @@ -452,8 +458,8 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
if (question.isPlainText !== undefined) {
isPlainText = question.isPlainText;
} else if (question.parsedSettings) {
isPlainText = question.parsedSettings.responseformat == 'monospaced' ||
question.parsedSettings.responseformat == 'plain';
isPlainText = question.parsedSettings.responseformat === 'monospaced' ||
question.parsedSettings.responseformat === 'plain';
} else {
const questionEl = convertTextToHTMLElement(question.html);
isPlainText = !!questionEl.querySelector('.qtype_essay_monospaced') || !!questionEl.querySelector('.qtype_essay_plain');
Expand Down
18 changes: 16 additions & 2 deletions src/addons/qtype/gapselect/services/handlers/gapselect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Injectable, Type } from '@angular/core';

import { CoreQuestion, CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question';
import { CoreQuestionHandler } from '@features/question/services/question-delegate';
import { makeSingleton } from '@singletons';
import { makeSingleton, Translate } from '@singletons';

/**
* Handler to support gapselect question type.
Expand Down Expand Up @@ -82,7 +82,7 @@ export class AddonQtypeGapSelectHandlerService implements CoreQuestionHandler {
// We should always get a value for each select so we can assume we receive all the possible answers.
for (const name in answers) {
const value = answers[name];
if (value) {
if (value && value !== '0') {
return 1;
}
}
Expand All @@ -101,6 +101,20 @@ export class AddonQtypeGapSelectHandlerService implements CoreQuestionHandler {
return CoreQuestion.compareAllAnswers(prevAnswers, newAnswers);
}

/**
* @inheritdoc
*/
getValidationError(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
): string | undefined {
if (this.isCompleteResponse(question, answers)) {
return;
}

return Translate.instant('addon.qtype_gapselect.pleaseputananswerineachbox');
}

}

export const AddonQtypeGapSelectHandler = makeSingleton(AddonQtypeGapSelectHandlerService);
Loading

0 comments on commit 59d38dc

Please sign in to comment.