commit
						bfdee4b5e2
					
				| @ -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", | ||||
|  | ||||
| @ -79,6 +79,15 @@ | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
| 
 | ||||
|         <!-- Word count info. --> | ||||
|         <ion-item class="ion-text-wrap" *ngIf="essayQuestion.wordCountInfo"> | ||||
|             <ion-label> | ||||
|                 <core-format-text [component]="component" [componentId]="componentId" [text]="essayQuestion.wordCountInfo" | ||||
|                     [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId"> | ||||
|                 </core-format-text> | ||||
|             </ion-label> | ||||
|         </ion-item> | ||||
| 
 | ||||
|         <!-- Answer plagiarism. --> | ||||
|         <ion-item class="ion-text-wrap" *ngIf="essayQuestion.answerPlagiarism"> | ||||
|             <ion-label> | ||||
|  | ||||
							
								
								
									
										4
									
								
								src/addons/qtype/essay/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/addons/qtype/essay/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -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." | ||||
| } | ||||
| @ -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, <string> 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, <string> answers.answer, undefined)) { | ||||
|             return 0; | ||||
|         } | ||||
| 
 | ||||
|         if (!allowedOptions.attachments) { | ||||
|             return hasTextAnswer ? 1 : 0; | ||||
|         } | ||||
|  | ||||
| @ -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.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -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. | ||||
|      * | ||||
|  | ||||
| @ -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); | ||||
| 
 | ||||
|  | ||||
| @ -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<CoreQuestionHandle | ||||
|         await this.executeFunctionOnEnabled(type, 'prepareSyncData', [question, answers, component, componentId, siteId]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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 { | ||||
|         const type = this.getTypeName(question); | ||||
| 
 | ||||
|         return this.executeFunctionOnEnabled(type, 'getValidationError', [question, answers, onlineError, component, componentId]); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const CoreQuestionDelegate = makeSingleton(CoreQuestionDelegateService); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user