From 0034b4fd060f733e1460f31d272a1241344d68e2 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 30 Apr 2021 13:15:40 +0200 Subject: [PATCH] MOBILE-3680 qtype_essay: Support word count in offline --- scripts/langindex.json | 17 +++++ .../essay/component/addon-qtype-essay.html | 9 +++ src/addons/qtype/essay/lang.json | 4 ++ .../qtype/essay/services/handlers/essay.ts | 63 ++++++++++++++++++- .../classes/base-question-component.ts | 2 + .../question/classes/base-question-handler.ts | 11 ++++ .../question/components/question/question.ts | 22 ++++--- .../question/services/question-delegate.ts | 40 ++++++++++++ 8 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 src/addons/qtype/essay/lang.json diff --git a/scripts/langindex.json b/scripts/langindex.json index 3dd04a77c..db5d12893 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -594,6 +594,7 @@ "addon.mod_forum.errorgetforum": "local_moodlemobileapp", "addon.mod_forum.errorgetgroups": "local_moodlemobileapp", "addon.mod_forum.errorposttoallgroups": "local_moodlemobileapp", + "addon.mod_forum.favourites": "forum", "addon.mod_forum.favouriteupdated": "forum", "addon.mod_forum.forumnodiscussionsyet": "local_moodlemobileapp", "addon.mod_forum.group": "local_moodlemobileapp", @@ -1004,6 +1005,10 @@ "addon.mod_workshop.switchphase30": "workshop", "addon.mod_workshop.switchphase40": "workshop", "addon.mod_workshop.switchphase50": "workshop", + "addon.mod_workshop.taskdone": "workshop", + "addon.mod_workshop.taskfail": "workshop", + "addon.mod_workshop.taskinfo": "workshop", + "addon.mod_workshop.tasktodo": "workshop", "addon.mod_workshop.userplan": "workshop", "addon.mod_workshop.userplancurrentphase": "workshop", "addon.mod_workshop.warningassessmentmodified": "local_moodlemobileapp", @@ -1044,12 +1049,15 @@ "addon.notifications.notifications": "local_moodlemobileapp", "addon.notifications.playsound": "local_moodlemobileapp", "addon.notifications.therearentnotificationsyet": "local_moodlemobileapp", + "addon.notifications.unreadnotification": "message", "addon.privatefiles.couldnotloadfiles": "local_moodlemobileapp", "addon.privatefiles.emptyfilelist": "local_moodlemobileapp", "addon.privatefiles.erroruploadnotworking": "local_moodlemobileapp", "addon.privatefiles.files": "moodle", "addon.privatefiles.privatefiles": "moodle", "addon.privatefiles.sitefiles": "moodle", + "addon.qtype_essay.maxwordlimitboundary": "qtype_essay", + "addon.qtype_essay.minwordlimitboundary": "qtype_essay", "addon.storagemanager.deletecourse": "local_moodlemobileapp", "addon.storagemanager.deletecourses": "local_moodlemobileapp", "addon.storagemanager.deletedatafrom": "local_moodlemobileapp", @@ -1389,6 +1397,7 @@ "core.clicktohideshow": "moodle", "core.clicktoseefull": "local_moodlemobileapp", "core.close": "repository", + "core.collapse": "moodle", "core.comments": "moodle", "core.comments.addcomment": "moodle", "core.comments.comments": "moodle", @@ -1573,6 +1582,7 @@ "core.errorsyncblocked": "local_moodlemobileapp", "core.errorurlschemeinvalidscheme": "local_moodlemobileapp", "core.errorurlschemeinvalidsite": "local_moodlemobileapp", + "core.expand": "moodle", "core.explanationdigitalminor": "moodle", "core.favourites": "moodle", "core.filename": "repository", @@ -1610,16 +1620,22 @@ "core.forcepasswordchangenotice": "moodle", "core.fulllistofcourses": "moodle", "core.fullnameandsitename": "local_moodlemobileapp", + "core.grades.aggregatemean": "grades", + "core.grades.aggregatesum": "grades", "core.grades.average": "grades", "core.grades.badgrade": "grades", + "core.grades.calculatedgrade": "grades", + "core.grades.category": "grades", "core.grades.contributiontocoursetotal": "grades", "core.grades.feedback": "grades", "core.grades.grade": "grades", "core.grades.gradeitem": "grades", "core.grades.grades": "grades", "core.grades.lettergrade": "grades", + "core.grades.manualitem": "grades", "core.grades.nogradesreturned": "grades", "core.grades.nooutcome": "grades", + "core.grades.outcome": "grades", "core.grades.percentage": "grades", "core.grades.range": "grades", "core.grades.rank": "grades", @@ -2138,6 +2154,7 @@ "core.time": "moodle", "core.timesup": "quiz", "core.today": "moodle", + "core.toggledelete": "local_moodlemobileapp", "core.tryagain": "local_moodlemobileapp", "core.twoparagraphs": "local_moodlemobileapp", "core.uhoh": "local_moodlemobileapp", diff --git a/src/addons/qtype/essay/component/addon-qtype-essay.html b/src/addons/qtype/essay/component/addon-qtype-essay.html index a6b80ce10..820882739 100644 --- a/src/addons/qtype/essay/component/addon-qtype-essay.html +++ b/src/addons/qtype/essay/component/addon-qtype-essay.html @@ -79,6 +79,15 @@ + + + + + + + + diff --git a/src/addons/qtype/essay/lang.json b/src/addons/qtype/essay/lang.json new file mode 100644 index 000000000..e0ed42573 --- /dev/null +++ b/src/addons/qtype/essay/lang.json @@ -0,0 +1,4 @@ +{ + "maxwordlimitboundary": "The word limit for this question is {{$a.limit}} words and you are attempting to submit {{$a.count}} words. Please shorten your response and try again.", + "minwordlimitboundary": "This question requires a response of at least {{$a.limit}} words and you are attempting to submit {{$a.count}} words. Please expand your response and try again." +} diff --git a/src/addons/qtype/essay/services/handlers/essay.ts b/src/addons/qtype/essay/services/handlers/essay.ts index e651af56b..cb6c91646 100644 --- a/src/addons/qtype/essay/services/handlers/essay.ts +++ b/src/addons/qtype/essay/services/handlers/essay.ts @@ -26,7 +26,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSFile } from '@services/ws'; -import { makeSingleton } from '@singletons'; +import { makeSingleton, Translate } from '@singletons'; import { AddonQtypeEssayComponent } from '../../component/essay'; /** @@ -155,6 +155,63 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { } } + /** + * @inheritdoc + */ + getValidationError( + question: CoreQuestionQuestionParsed, + answers: CoreQuestionsAnswers, + onlineError: string | undefined, + ): string | undefined { + if (answers.answer === undefined) { + // Not answered in offline. + return onlineError; + } + + if (!answers.answer) { + // Not answered yet, no error. + return; + } + + return this.checkInputWordCount(question, answers.answer, onlineError); + } + + /** + * Check the input word count and return a message to user when the number of words are outside the boundary settings. + * + * @param question The question. + * @param answers Object with the question answers (without prefix). + * @param onlineError Online validation error. + * @return Error message if there's a validation error, undefined otherwise. + */ + protected checkInputWordCount( + question: CoreQuestionQuestionParsed, + answer: string, + onlineError: string | undefined, + ): string | undefined { + if (!question.parsedSettings || question.parsedSettings.maxwordlimit === undefined || + question.parsedSettings.minwordlimit === undefined) { + // Min/max not supported, use online error. + return onlineError; + } + + const minWords = Number(question.parsedSettings.minwordlimit); + const maxWords = Number(question.parsedSettings.maxwordlimit); + + if (!maxWords && !minWords) { + // No min and max, no error. + return; + } + + // Count the number of words in the response string. + const count = CoreTextUtils.countWords(answer); + if (maxWords && count > maxWords) { + return Translate.instant('addon.qtype_essay.maxwordlimitboundary', { $a: { limit: maxWords, count: count } }); + } else if (count < minWords) { + return Translate.instant('addon.qtype_essay.minwordlimitboundary', { $a: { limit: minWords, count: count } }); + } + } + /** * Check if a response is complete. * @@ -175,6 +232,10 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { const uploadFilesSupported = typeof question.responsefileareas != 'undefined'; const allowedOptions = this.getAllowedOptions(question); + if (hasTextAnswer && this.checkInputWordCount(question, answers.answer, undefined)) { + return 0; + } + if (!allowedOptions.attachments) { return hasTextAnswer ? 1 : 0; } diff --git a/src/core/features/question/classes/base-question-component.ts b/src/core/features/question/classes/base-question-component.ts index dcb5f9458..2c8013b83 100644 --- a/src/core/features/question/classes/base-question-component.ts +++ b/src/core/features/question/classes/base-question-component.ts @@ -264,6 +264,7 @@ export class CoreQuestionBaseComponent { if (review) { // Search the answer and the attachments. question.answer = CoreDomUtils.getContentsOfElement(questionEl, '.qtype_essay_response'); + question.wordCountInfo = questionEl.querySelector('.answer > p')?.innerHTML; if (question.parsedSettings) { question.attachments = Array.from( @@ -764,6 +765,7 @@ export type AddonModQuizEssayQuestion = AddonModQuizQuestionBasicData & { attachmentsMaxBytes?: number; // Max bytes for attachments. answerPlagiarism?: string; // Plagiarism HTML for the answer. attachmentsPlagiarisms?: string[]; // Plagiarism HTML for each attachment. + wordCountInfo?: string; // Info about word count. }; /** diff --git a/src/core/features/question/classes/base-question-handler.ts b/src/core/features/question/classes/base-question-handler.ts index a7a53b446..bd062e645 100644 --- a/src/core/features/question/classes/base-question-handler.ts +++ b/src/core/features/question/classes/base-question-handler.ts @@ -75,6 +75,17 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler { return; } + /** + * @inheritdoc + */ + getValidationError( + question: CoreQuestionQuestionParsed, + answers: CoreQuestionsAnswers, + onlineError: string, + ): string | undefined { + return onlineError; + } + /** * Check if a response is complete. * diff --git a/src/core/features/question/components/question/question.ts b/src/core/features/question/components/question/question.ts index 0887de6bc..4d17d9ef4 100644 --- a/src/core/features/question/components/question/question.ts +++ b/src/core/features/question/components/question/question.ts @@ -128,18 +128,26 @@ export class CoreQuestionComponent implements OnInit { return; } - // Load local answers if offline is enabled. - if (this.offlineEnabled && this.component && this.attemptId) { - await CoreQuestionHelper.loadLocalAnswers(this.question, this.component, this.attemptId); - } else { - this.question.localAnswers = {}; - } - CoreQuestionHelper.extractQbehaviourRedoButton(this.question); // Extract the validation error of the question. this.validationError = CoreQuestionHelper.getValidationErrorFromHtml(this.question.html); + // Load local answers if offline is enabled. + if (this.offlineEnabled && this.component && this.attemptId) { + await CoreQuestionHelper.loadLocalAnswers(this.question, this.component, this.attemptId); + + this.validationError = CoreQuestionDelegate.getValidationError( + this.question, + this.question.localAnswers || {}, + this.validationError, + this.component, + this.attemptId, + ); + } else { + this.question.localAnswers = {}; + } + // Load the local answers in the HTML. CoreQuestionHelper.loadLocalAnswersInHtml(this.question); diff --git a/src/core/features/question/services/question-delegate.ts b/src/core/features/question/services/question-delegate.ts index ff3d1bc01..f9a3ab2cb 100644 --- a/src/core/features/question/services/question-delegate.ts +++ b/src/core/features/question/services/question-delegate.ts @@ -57,6 +57,24 @@ export interface CoreQuestionHandler extends CoreDelegateHandler { */ getPreventSubmitMessage?(question: CoreQuestionQuestionParsed): string | undefined; + /** + * Check if there's a validation error with the offline data. + * + * @param question The question. + * @param answers Object with the question offline answers (without prefix). + * @param onlineError Online validation error. + * @param component The component the question is related to. + * @param componentId Component ID. + * @return Error message if there's a validation error, undefined otherwise. + */ + getValidationError?( + question: CoreQuestionQuestionParsed, + answers: CoreQuestionsAnswers, + onlineError: string | undefined, + component: string, + componentId: string | number, + ): string | undefined; + /** * Check if a response is complete. * @@ -455,6 +473,28 @@ export class CoreQuestionDelegateService extends CoreDelegate