diff --git a/scripts/langindex.json b/scripts/langindex.json
index 4a31ebf4178..272d14f62e8 100644
--- a/scripts/langindex.json
+++ b/scripts/langindex.json
@@ -1148,9 +1148,22 @@
"addon.privatefiles.files": "moodle",
"addon.privatefiles.privatefiles": "moodle",
"addon.privatefiles.sitefiles": "moodle",
+ "addon.qtype_ddimageortext.pleasedraganimagetoeachdropregion": "qtype_ddimageortext",
+ "addon.qtype_ddmarker.pleasedragatleastonemarker": "qtype_ddmarker",
"addon.qtype_essay.maxwordlimitboundary": "qtype_essay",
"addon.qtype_essay.minwordlimitboundary": "qtype_essay",
+ "addon.qtype_gapselect.pleaseputananswerineachbox": "qtype_gapselect",
+ "addon.qtype_match.pleaseananswerallparts": "qtype_match",
+ "addon.qtype_multianswer.pleaseananswerallparts": "qtype_multianswer",
+ "addon.qtype_multichoice.pleaseselectananswer": "qtype_multichoice",
+ "addon.qtype_multichoice.pleaseselectatleastoneanswer": "qtype_multichoice",
+ "addon.qtype_numerical.invalidnumber": "qtype_numerical",
+ "addon.qtype_numerical.invalidnumbernounit": "qtype_numerical",
+ "addon.qtype_numerical.pleaseenterananswer": "qtype_numerical",
+ "addon.qtype_numerical.unitnotselected": "qtype_numerical",
"addon.qtype_ordering.moved": "qtype_ordering",
+ "addon.qtype_shortanswer.pleaseenterananswer": "qtype_shortanswer",
+ "addon.qtype_truefalse.pleaseselectananswer": "qtype_truefalse",
"addon.report_insights.actionsaved": "report_insights",
"addon.report_insights.fixedack": "analytics",
"addon.report_insights.incorrectlyflagged": "analytics",
diff --git a/src/addons/mod/book/pages/contents/contents.html b/src/addons/mod/book/pages/contents/contents.html
index 83191f3c514..a03124df1d2 100644
--- a/src/addons/mod/book/pages/contents/contents.html
+++ b/src/addons/mod/book/pages/contents/contents.html
@@ -10,7 +10,7 @@
-
+
diff --git a/src/addons/mod/book/tests/behat/snapshots/test-basic-usage-of-book-activity-in-app-open-chapters-from-table-of-contents_11.png b/src/addons/mod/book/tests/behat/snapshots/test-basic-usage-of-book-activity-in-app-open-chapters-from-table-of-contents_11.png
index 41667409394..63dc621e5d9 100644
Binary files a/src/addons/mod/book/tests/behat/snapshots/test-basic-usage-of-book-activity-in-app-open-chapters-from-table-of-contents_11.png and b/src/addons/mod/book/tests/behat/snapshots/test-basic-usage-of-book-activity-in-app-open-chapters-from-table-of-contents_11.png differ
diff --git a/src/addons/mod/imscp/pages/view/view.html b/src/addons/mod/imscp/pages/view/view.html
index 611732ec94d..6e6eb66eede 100644
--- a/src/addons/mod/imscp/pages/view/view.html
+++ b/src/addons/mod/imscp/pages/view/view.html
@@ -11,7 +11,7 @@
-
+
diff --git a/src/addons/mod/lesson/pages/player/player.html b/src/addons/mod/lesson/pages/player/player.html
index f56047a6c41..0e2166e3583 100644
--- a/src/addons/mod/lesson/pages/player/player.html
+++ b/src/addons/mod/lesson/pages/player/player.html
@@ -11,7 +11,7 @@
-
+
diff --git a/src/addons/mod/quiz/accessrules/password/services/handlers/password.ts b/src/addons/mod/quiz/accessrules/password/services/handlers/password.ts
index 7e0fec67f2e..5ff4cc59a7e 100644
--- a/src/addons/mod/quiz/accessrules/password/services/handlers/password.ts
+++ b/src/addons/mod/quiz/accessrules/password/services/handlers/password.ts
@@ -32,14 +32,7 @@ export class AddonModQuizAccessPasswordHandlerService implements AddonModQuizAcc
ruleName = 'quizaccess_password';
/**
- * Add preflight data that doesn't require user interaction. The data should be added to the preflightData param.
- *
- * @param quiz The quiz the rule belongs to.
- * @param preflightData Object where to add the preflight data.
- * @param attempt The attempt started/continued. If not supplied, user is starting a new attempt.
- * @param prefetch Whether the user is prefetching the quiz.
- * @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when done if async, void if it's synchronous.
+ * @inheritdoc
*/
async getFixedPreflightData(
quiz: AddonModQuizQuizWSData,
@@ -76,33 +69,21 @@ export class AddonModQuizAccessPasswordHandlerService implements AddonModQuizAcc
}
/**
- * Return the Component to use to display the access rule preflight.
- * Implement this if your access rule requires a preflight check with user interaction.
- * It's recommended to return the class of the component, but you can also return an instance of the component.
- *
- * @returns The component (or promise resolved with component) to use, undefined if not found.
+ * @inheritdoc
*/
getPreflightComponent(): Type | Promise> {
return AddonModQuizAccessPasswordComponent;
}
/**
- * Whether or not the handler is enabled on a site level.
- *
- * @returns True or promise resolved with true if enabled.
+ * @inheritdoc
*/
async isEnabled(): Promise {
return true;
}
/**
- * Whether the rule requires a preflight check when prefetch/start/continue an attempt.
- *
- * @param quiz The quiz the rule belongs to.
- * @param attempt The attempt started/continued. If not supplied, user is starting a new attempt.
- * @param prefetch Whether the user is prefetching the quiz.
- * @param siteId Site ID. If not defined, current site.
- * @returns Whether the rule requires a preflight check.
+ * @inheritdoc
*/
async isPreflightCheckRequired(
quiz: AddonModQuizQuizWSData,
@@ -117,14 +98,7 @@ export class AddonModQuizAccessPasswordHandlerService implements AddonModQuizAcc
}
/**
- * Function called when the preflight check has passed. This is a chance to record that fact in some way.
- *
- * @param quiz The quiz the rule belongs to.
- * @param attempt The attempt started/continued.
- * @param preflightData Preflight data gathered.
- * @param prefetch Whether the user is prefetching the quiz.
- * @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when done if async, void if it's synchronous.
+ * @inheritdoc
*/
async notifyPreflightCheckPassed(
quiz: AddonModQuizQuizWSData,
@@ -135,21 +109,14 @@ export class AddonModQuizAccessPasswordHandlerService implements AddonModQuizAcc
): Promise {
// The password is right, store it to use it automatically in following executions.
if (preflightData.quizpassword !== undefined) {
- return this.storePassword(quiz.id, preflightData.quizpassword, siteId);
+ await this.storePassword(quiz.id, preflightData.quizpassword, siteId);
}
}
/**
- * Function called when the preflight check fails. This is a chance to record that fact in some way.
- *
- * @param quiz The quiz the rule belongs to.
- * @param attempt The attempt started/continued.
- * @param preflightData Preflight data gathered.
- * @param prefetch Whether the user is prefetching the quiz.
- * @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when done if async, void if it's synchronous.
+ * @inheritdoc
*/
- notifyPreflightCheckFailed?(
+ async notifyPreflightCheckFailed?(
quiz: AddonModQuizQuizWSData,
attempt: AddonModQuizAttemptWSData | undefined,
preflightData: Record,
@@ -157,7 +124,7 @@ export class AddonModQuizAccessPasswordHandlerService implements AddonModQuizAcc
siteId?: string,
): Promise {
// The password is wrong, remove it from DB if it's there.
- return this.removePassword(quiz.id, siteId);
+ await this.removePassword(quiz.id, siteId);
}
/**
@@ -165,7 +132,6 @@ export class AddonModQuizAccessPasswordHandlerService implements AddonModQuizAcc
*
* @param quizId Quiz ID.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when done.
*/
protected async removePassword(quizId: number, siteId?: string): Promise {
const site = await CoreSites.getSite(siteId);
@@ -179,7 +145,6 @@ export class AddonModQuizAccessPasswordHandlerService implements AddonModQuizAcc
* @param quizId Quiz ID.
* @param password Password.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when done.
*/
protected async storePassword(quizId: number, password: string, siteId?: string): Promise {
const site = await CoreSites.getSite(siteId);
diff --git a/src/addons/mod/quiz/components/index/index.ts b/src/addons/mod/quiz/components/index/index.ts
index 30515e84b40..112f1af1b07 100644
--- a/src/addons/mod/quiz/components/index/index.ts
+++ b/src/addons/mod/quiz/components/index/index.ts
@@ -130,8 +130,6 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
/**
* Attempt the quiz.
- *
- * @returns Promise resolved when done.
*/
async attemptQuiz(): Promise {
if (this.showStatusSpinner || !this.quiz) {
@@ -141,7 +139,9 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
if (!AddonModQuiz.isQuizOffline(this.quiz)) {
// Quiz isn't offline, just open it.
- return this.openQuiz();
+ this.openQuiz();
+
+ return;
}
// Quiz supports offline, check if it needs to be downloaded.
@@ -150,7 +150,9 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
if (isDownloaded) {
// Already downloaded, open it.
- return this.openQuiz();
+ this.openQuiz();
+
+ return;
}
// Prefetch the quiz.
@@ -242,7 +244,6 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
* Get the user attempts in the quiz and the result info.
*
* @param quiz Quiz instance.
- * @returns Promise resolved when done.
*/
protected async getAttempts(
quiz: AddonModQuizQuizData,
@@ -326,7 +327,6 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
* Get result info to show.
*
* @param quiz Quiz.
- * @returns Promise resolved when done.
*/
protected async getResultInfo(quiz: AddonModQuizQuizData): Promise {
if (!this.attempts.length || !quiz.showAttemptsGrades || !this.bestGrade?.hasgrade ||
@@ -382,8 +382,6 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
/**
* Go to review an attempt that has just been finished.
- *
- * @returns Promise resolved when done.
*/
protected async goToAutoReview(attempts: AddonModQuizAttemptWSData[]): Promise {
if (!this.autoReview) {
@@ -505,10 +503,10 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
/**
* Open a quiz to attempt it.
*/
- protected openQuiz(): void {
+ protected async openQuiz(): Promise {
this.hasPlayed = true;
- CoreNavigator.navigateToSitePath(
+ await CoreNavigator.navigateToSitePath(
`${ADDON_MOD_QUIZ_PAGE_NAME}/${this.courseId}/${this.module.id}/player`,
{
params: {
@@ -554,7 +552,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
* @param quiz Quiz data.
* @param accessInfo Quiz access information.
* @param attempts The attempts to treat.
- * @returns Promise resolved when done.
+ * @returns Formatted attempts.
*/
protected async treatAttempts(
quiz: AddonModQuizQuizData,
@@ -630,8 +628,6 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
/**
* Get quiz grade data.
- *
- * @returns Promise resolved when done.
*/
protected async getQuizGrade(): Promise {
try {
@@ -656,8 +652,6 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
/**
* Go to page to review the attempt.
- *
- * @returns Promise resolved when done.
*/
async reviewAttempt(attemptId: number): Promise {
await CoreNavigator.navigateToSitePath(
diff --git a/src/addons/mod/quiz/constants.ts b/src/addons/mod/quiz/constants.ts
index 67bd89826cc..8dc0d8775c5 100644
--- a/src/addons/mod/quiz/constants.ts
+++ b/src/addons/mod/quiz/constants.ts
@@ -54,3 +54,11 @@ export const enum AddonModQuizDisplayOptionsAttemptStates {
LATER_WHILE_OPEN = 0x00100,
AFTER_CLOSE = 0x00010,
}
+
+/**
+ * Possible navigation methods for a quiz.
+ */
+export const enum AddonModQuizNavMethods {
+ FREE = 'free',
+ SEQ = 'sequential',
+}
diff --git a/src/addons/mod/quiz/pages/player/player.html b/src/addons/mod/quiz/pages/player/player.html
index 899bd0cfbae..60572189665 100644
--- a/src/addons/mod/quiz/pages/player/player.html
+++ b/src/addons/mod/quiz/pages/player/player.html
@@ -15,9 +15,9 @@
(click)="showConnectionError($event)" [ariaLabel]="'addon.mod_quiz.connectionerror' | translate" aria-haspopup="dialog">
-
-
+
@@ -64,13 +64,13 @@
-
+
{{ 'addon.mod_quiz.summaryofattempt' | translate }}
-
+
-
+
diff --git a/src/addons/mod/quiz/pages/player/player.ts b/src/addons/mod/quiz/pages/player/player.ts
index 3eaadd32529..3f5fb9325a7 100644
--- a/src/addons/mod/quiz/pages/player/player.ts
+++ b/src/addons/mod/quiz/pages/player/player.ts
@@ -20,7 +20,6 @@ import { CoreIonLoadingElement } from '@classes/ion-loading';
import { CoreQuestionComponent } from '@features/question/components/question/question';
import {
CoreQuestionQuestionForView,
- CoreQuestionQuestionParsed,
CoreQuestionsAnswers,
} from '@features/question/services/question';
import { CoreQuestionBehaviourButton, CoreQuestionHelper } from '@features/question/services/question-helper';
@@ -78,12 +77,11 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
loaded = false; // Whether data has been loaded.
quizAborted = false; // Whether the quiz was aborted due to an error.
offline = false; // Whether the quiz is being attempted in offline mode.
- navigation: AddonModQuizNavigationQuestion[] = []; // List of questions to navigate them.
+ attemptSummary: AddonModQuizNavigationQuestion[] = []; // Attempt summary: list of questions to navigate.
questions: CoreQuestionQuestionForView[] = []; // Questions of the current page.
nextPage = -2; // Next page.
previousPage = -1; // Previous page.
showSummary = false; // Whether the attempt summary should be displayed.
- summaryQuestions: CoreQuestionQuestionParsed[] = []; // The questions to display in the summary.
canReturn = false; // Whether the user can return to a page after seeing the summary.
preventSubmitMessages: string[] = []; // List of messages explaining why the quiz cannot be submitted.
endTime?: number; // The time when the attempt must be finished.
@@ -266,24 +264,23 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
* @param page Page to load. -1 means summary.
* @param fromModal Whether the page was selected using the navigation modal.
* @param slot Slot of the question to scroll to.
- * @returns Promise resolved when done.
*/
async changePage(page: number, fromModal?: boolean, slot?: number): Promise {
if (!this.attempt) {
return;
}
- if (page != -1 && (this.attempt.state === AddonModQuizAttemptStates.OVERDUE || this.attempt.finishedOffline)) {
+ if (page !== -1 && (this.attempt.state === AddonModQuizAttemptStates.OVERDUE || this.attempt.finishedOffline)) {
// We can't load a page if overdue or the local attempt is finished.
return;
- } else if (page == this.attempt.currentpage && !this.showSummary && slot !== undefined) {
+ } else if (page === this.attempt.currentpage && !this.showSummary && slot !== undefined) {
// Navigating to a question in the current page.
await this.scrollToQuestion(slot);
return;
} else if (
- (page == this.attempt.currentpage && !this.showSummary) ||
- (fromModal && this.isSequential && page != this.attempt.currentpage && page !== this.nextPage)
+ (page === this.attempt.currentpage && !this.showSummary) ||
+ (fromModal && this.isSequential && page !== this.attempt.currentpage && page !== this.nextPage)
) {
// If the user is navigating to the current page we do nothing.
// Also, in sequential quizzes we can only navigate to the current page.
@@ -344,8 +341,6 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
/**
* Convenience function to get the quiz data.
- *
- * @returns Promise resolved when done.
*/
protected async fetchData(): Promise {
this.quiz = await AddonModQuiz.getQuiz(this.courseId, this.cmId);
@@ -403,7 +398,6 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
*
* @param userFinish Whether the user clicked to finish the attempt.
* @param timeUp Whether the quiz time is up.
- * @returns Promise resolved when done.
*/
async finishAttempt(userFinish?: boolean, timeUp?: boolean): Promise {
if (!this.quiz || !this.attempt) {
@@ -417,7 +411,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
if (!timeUp && this.attempt.state === AddonModQuizAttemptStates.IN_PROGRESS) {
let message = Translate.instant('addon.mod_quiz.confirmclose');
- const unansweredCount = this.summaryQuestions
+ const unansweredCount = this.attemptSummary
.filter(question => AddonModQuiz.isQuestionUnanswered(question))
.length;
@@ -490,8 +484,6 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
/**
* Fix sequence checks of current page.
- *
- * @returns Promise resolved when done.
*/
protected async fixSequenceChecks(): Promise {
if (!this.attempt) {
@@ -561,7 +553,6 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
* Load a page questions.
*
* @param page The page to load.
- * @returns Promise resolved when done.
*/
protected async loadPage(page: number): Promise {
if (!this.quiz || !this.attempt) {
@@ -668,8 +659,6 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
/**
* Load attempt summary.
- *
- * @returns Promise resolved when done.
*/
protected async loadSummary(): Promise {
if (!this.quiz || !this.attempt) {
@@ -682,21 +671,11 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
this.partialCorrectIcon = CoreQuestionHelper.getPartiallyCorrectIcon().fullName;
}
- this.summaryQuestions = [];
-
- this.summaryQuestions = await AddonModQuiz.getAttemptSummary(this.attempt.id, this.preflightData, {
- cmId: this.quiz.coursemodule,
- loadLocal: this.offline,
- readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK,
- });
-
- this.summaryQuestions.forEach((question) => {
- CoreQuestionHelper.populateQuestionStateClass(question);
- });
+ await this.loadAttemptSummary();
this.showSummary = true;
this.canReturn = this.attempt.state === AddonModQuizAttemptStates.IN_PROGRESS && !this.attempt.finishedOffline;
- this.preventSubmitMessages = AddonModQuiz.getPreventSubmitMessages(this.summaryQuestions);
+ this.preventSubmitMessages = AddonModQuiz.getPreventSubmitMessages(this.attemptSummary);
this.dueDateWarning = AddonModQuiz.getAttemptDueDateWarning(this.quiz, this.attempt);
@@ -704,31 +683,27 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
}
/**
- * Load data to navigate the questions using the navigation modal.
- *
- * @returns Promise resolved when done.
+ * Load attempt summary data.
*/
- protected async loadNavigation(): Promise {
- if (!this.attempt) {
+ protected async loadAttemptSummary(): Promise {
+ if (!this.quiz || !this.attempt) {
return;
}
// We use the attempt summary to build the navigation because it contains all the questions.
- this.navigation = await AddonModQuiz.getAttemptSummary(this.attempt.id, this.preflightData, {
- cmId: this.quiz?.coursemodule,
+ this.attemptSummary = await AddonModQuiz.getAttemptSummary(this.attempt.id, this.preflightData, {
+ cmId: this.quiz.coursemodule,
loadLocal: this.offline,
readingStrategy: this.offline ? CoreSitesReadingStrategy.PREFER_CACHE : CoreSitesReadingStrategy.ONLY_NETWORK,
});
- this.navigation.forEach((question) => {
+ this.attemptSummary.forEach((question) => {
CoreQuestionHelper.populateQuestionStateClass(question);
});
}
/**
* Open the navigation modal.
- *
- * @returns Promise resolved when done.
*/
async openNavigation(): Promise {
@@ -736,7 +711,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
// Some data has changed, reload the navigation.
const modal = await CoreLoadings.show();
- await CorePromiseUtils.ignoreErrors(this.loadNavigation());
+ await CorePromiseUtils.ignoreErrors(this.loadAttemptSummary());
modal.dismiss();
this.reloadNavigation = false;
@@ -748,7 +723,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
const modalData = await CoreModals.openSideModal({
component: AddonModQuizNavigationModalComponent,
componentProps: {
- navigation: this.navigation,
+ navigation: this.attemptSummary,
summaryShown: this.showSummary,
currentPage: this.attempt?.currentpage,
nextPage: this.nextPage,
@@ -786,7 +761,6 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
* @param userFinish Whether the user clicked to finish the attempt.
* @param timeUp Whether the quiz time is up.
* @param retrying Whether we're retrying the change.
- * @returns Promise resolved when done.
*/
protected async processAttempt(userFinish?: boolean, timeUp?: boolean, retrying?: boolean): Promise {
if (!this.quiz || !this.attempt) {
@@ -829,7 +803,9 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
}
// Sequence checks updated, try to send the data again.
- return this.processAttempt(userFinish, timeUp, true);
+ await this.processAttempt(userFinish, timeUp, true);
+
+ return;
}
// Answers saved, cancel auto save.
@@ -840,7 +816,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
CoreForms.triggerFormSubmittedEvent(this.formElement, !this.offline, CoreSites.getCurrentSiteId());
}
- return CoreQuestionHelper.clearTmpData(this.questions, this.component, this.quiz.coursemodule);
+ await CoreQuestionHelper.clearTmpData(this.questions, this.component, this.quiz.coursemodule);
}
/**
@@ -891,8 +867,6 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
/**
* Start or continue an attempt.
- *
- * @returns Promise resolved when done.
*/
protected async startOrContinueAttempt(): Promise {
if (!this.quiz || !this.quizAccessInfo) {
@@ -922,7 +896,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy, CanLeave {
this.attempt = attempt;
- await this.loadNavigation();
+ await this.loadAttemptSummary();
if (this.attempt.state !== AddonModQuizAttemptStates.OVERDUE && !this.attempt.finishedOffline) {
// Attempt not overdue and not finished in offline, load page.
diff --git a/src/addons/mod/quiz/pages/review/review.html b/src/addons/mod/quiz/pages/review/review.html
index 65a6d4904aa..aec5aeaf682 100644
--- a/src/addons/mod/quiz/pages/review/review.html
+++ b/src/addons/mod/quiz/pages/review/review.html
@@ -10,7 +10,7 @@ {{ 'addon.mod_quiz.review' | translate }}
-
+
diff --git a/src/addons/mod/quiz/pages/review/review.ts b/src/addons/mod/quiz/pages/review/review.ts
index 82124e21150..44276641630 100644
--- a/src/addons/mod/quiz/pages/review/review.ts
+++ b/src/addons/mod/quiz/pages/review/review.ts
@@ -144,8 +144,6 @@ export class AddonModQuizReviewPage implements OnInit {
/**
* Convenience function to get the quiz data.
- *
- * @returns Promise resolved when done.
*/
protected async fetchData(): Promise {
try {
@@ -171,7 +169,6 @@ export class AddonModQuizReviewPage implements OnInit {
* Load a page questions.
*
* @param page The page to load.
- * @returns Promise resolved when done.
*/
protected async loadPage(page: number): Promise {
if (!this.quiz) {
@@ -200,8 +197,6 @@ export class AddonModQuizReviewPage implements OnInit {
/**
* Load data to navigate the questions using the navigation modal.
- *
- * @returns Promise resolved when done.
*/
protected async loadNavigation(): Promise {
// Get all questions in single page to retrieve all the questions.
diff --git a/src/addons/mod/quiz/services/access-rules-delegate.ts b/src/addons/mod/quiz/services/access-rules-delegate.ts
index 40b5f0c0155..e15ed792ac4 100644
--- a/src/addons/mod/quiz/services/access-rules-delegate.ts
+++ b/src/addons/mod/quiz/services/access-rules-delegate.ts
@@ -256,7 +256,6 @@ export class AddonModQuizAccessRuleDelegateService extends CoreDelegate {
return true;
}
/**
- * Prefetch a module.
- *
- * @param module Module.
- * @param courseId Course ID the module belongs to.
- * @param single True if we're downloading a single module, false if we're downloading a whole section.
- * @param dirPath Path of the directory where to store all the content files.
- * @param canStart If true, start a new attempt if needed.
- * @returns Promise resolved when done.
+ * @inheritdoc
*/
async prefetch(
module: SyncedModule,
@@ -292,7 +283,6 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
* @param single True if we're downloading a single module, false if we're downloading a whole section.
* @param canStart If true, start a new attempt if needed.
* @param siteId Site ID.
- * @returns Promise resolved when done.
*/
protected async prefetchQuiz(
module: CoreCourseAnyModuleData,
@@ -474,7 +464,6 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
* @param accessInfo Quiz access info.
* @param attempt Attempt.
* @param modOptions Other options.
- * @returns Promise resolved when done.
*/
protected async prefetchAttemptReview(
quiz: AddonModQuizQuizWSData,
@@ -511,7 +500,6 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
* @param quiz Quiz.
* @param attempt Attempt.
* @param modOptions Other options.
- * @returns Promise resolved when done.
*/
protected async prefetchAttemptReviewFiles(
quiz: AddonModQuizQuizWSData,
@@ -545,7 +533,6 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
* @param quiz Quiz.
* @param modOptions Other options.
* @param siteId Site ID.
- * @returns Promise resolved when done.
*/
protected async prefetchGradeAndFeedback(
quiz: AddonModQuizQuizWSData,
@@ -570,7 +557,6 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
* @param quiz Quiz.
* @param askPreflight Whether it should ask for preflight data if needed.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when done.
*/
async prefetchQuizAndLastAttempt(quiz: AddonModQuizQuizWSData, askPreflight?: boolean, siteId?: string): Promise {
siteId = siteId || CoreSites.getCurrentSiteId();
@@ -617,7 +603,6 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
*
* @param quiz Quiz.
* @param options Other options.
- * @returns Promise resolved when done.
*/
async setStatusAfterPrefetch(
quiz: AddonModQuizQuizWSData,
@@ -654,7 +639,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet
* @param module Module.
* @param courseId Course ID the module belongs to
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when done.
+ * @returns Sync results.
*/
async sync(module: SyncedModule, courseId: number, siteId?: string): Promise {
const quiz = await AddonModQuiz.getQuiz(courseId, module.id, { siteId });
diff --git a/src/addons/mod/quiz/services/handlers/push-click.ts b/src/addons/mod/quiz/services/handlers/push-click.ts
index 0b627910673..bc2aade7756 100644
--- a/src/addons/mod/quiz/services/handlers/push-click.ts
+++ b/src/addons/mod/quiz/services/handlers/push-click.ts
@@ -39,10 +39,7 @@ export class AddonModQuizPushClickHandlerService implements CorePushNotification
protected readonly SUPPORTED_NAMES = ['submission', 'confirmation', 'attempt_overdue'];
/**
- * Check if a notification click is handled by this handler.
- *
- * @param notification The notification to check.
- * @returns Whether the notification click is handled by this handler
+ * @inheritdoc
*/
async handles(notification: AddonModQuizPushNotificationData): Promise {
return CoreUtils.isTrueOrOne(notification.notif) && notification.moodlecomponent == 'mod_quiz' &&
@@ -51,10 +48,7 @@ export class AddonModQuizPushClickHandlerService implements CorePushNotification
}
/**
- * Handle the notification click.
- *
- * @param notification The notification to check.
- * @returns Promise resolved when done.
+ * @inheritdoc
*/
async handleClick(notification: AddonModQuizPushNotificationData): Promise {
const contextUrlParams = CoreUrl.extractUrlParams(notification.contexturl || '');
@@ -68,12 +62,14 @@ export class AddonModQuizPushClickHandlerService implements CorePushNotification
contextUrlParams.page !== undefined
) {
// A student made a submission, go to view the attempt.
- return AddonModQuizHelper.handleReviewLink(
+ await AddonModQuizHelper.handleReviewLink(
Number(contextUrlParams.attempt),
Number(contextUrlParams.page),
Number(data.instance),
notification.site,
);
+
+ return;
}
// Open the activity.
@@ -84,7 +80,7 @@ export class AddonModQuizPushClickHandlerService implements CorePushNotification
await CorePromiseUtils.ignoreErrors(AddonModQuiz.invalidateContent(moduleId, courseId, notification.site));
- return CoreCourseHelper.navigateToModule(moduleId, {
+ await CoreCourseHelper.navigateToModule(moduleId, {
courseId,
siteId: notification.site,
});
diff --git a/src/addons/mod/quiz/services/handlers/sync-cron.ts b/src/addons/mod/quiz/services/handlers/sync-cron.ts
index db8ef878edb..1421a9a2766 100644
--- a/src/addons/mod/quiz/services/handlers/sync-cron.ts
+++ b/src/addons/mod/quiz/services/handlers/sync-cron.ts
@@ -27,21 +27,14 @@ export class AddonModQuizSyncCronHandlerService implements CoreCronHandler {
name = 'AddonModQuizSyncCronHandler';
/**
- * Execute the process.
- * Receives the ID of the site affected, undefined for all sites.
- *
- * @param siteId ID of the site affected, undefined for all sites.
- * @param force Wether the execution is forced (manual sync).
- * @returns Promise resolved when done, rejected if failure.
+ * @inheritdoc
*/
execute(siteId?: string, force?: boolean): Promise {
return AddonModQuizSync.syncAllQuizzes(siteId, force);
}
/**
- * Get the time between consecutive executions.
- *
- * @returns Time between consecutive executions (in ms).
+ * @inheritdoc
*/
getInterval(): number {
return AddonModQuizSync.syncInterval;
diff --git a/src/addons/mod/quiz/services/quiz-helper.ts b/src/addons/mod/quiz/services/quiz-helper.ts
index 85ba4d5053f..7c616624f28 100644
--- a/src/addons/mod/quiz/services/quiz-helper.ts
+++ b/src/addons/mod/quiz/services/quiz-helper.ts
@@ -334,7 +334,6 @@ export class AddonModQuizHelperProvider {
* @param page Page to load, -1 to all questions in same page.
* @param quizId Quiz ID.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when done.
*/
async handleReviewLink(attemptId: number, page?: number, quizId?: number, siteId?: string): Promise {
siteId = siteId || CoreSites.getCurrentSiteId();
diff --git a/src/addons/mod/quiz/services/quiz-offline.ts b/src/addons/mod/quiz/services/quiz-offline.ts
index ff98e646a65..58bf97e7997 100644
--- a/src/addons/mod/quiz/services/quiz-offline.ts
+++ b/src/addons/mod/quiz/services/quiz-offline.ts
@@ -140,7 +140,7 @@ export class AddonModQuizOfflineProvider {
* @param attemptId Attempt ID.
* @param questions List of questions.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when done.
+ * @returns Questions with local states loaded.
*/
async loadQuestionsLocalStates(
attemptId: number,
@@ -223,7 +223,6 @@ export class AddonModQuizOfflineProvider {
*
* @param attemptId Attempt ID.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when done.
*/
async removeAttemptAndAnswers(attemptId: number, siteId?: string): Promise {
siteId = siteId || CoreSites.getCurrentSiteId();
@@ -263,7 +262,6 @@ export class AddonModQuizOfflineProvider {
* @param answers Answers to save.
* @param timeMod Time modified to set in the answers. If not defined, current time.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when done.
*/
async saveAnswers(
quiz: AddonModQuizQuizWSData,
diff --git a/src/addons/mod/quiz/services/quiz-sync.ts b/src/addons/mod/quiz/services/quiz-sync.ts
index 400dedbaa28..f348ad1ab77 100644
--- a/src/addons/mod/quiz/services/quiz-sync.ts
+++ b/src/addons/mod/quiz/services/quiz-sync.ts
@@ -139,7 +139,6 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
* @param quiz Quiz.
* @param courseId Course ID.
* @param siteId Site ID. If not defined, current site.
- * @returns Promise resolved when done.
*/
protected async prefetchAfterUpdateQuiz(
module: CoreCourseModuleBasicInfo,
@@ -187,7 +186,6 @@ export class AddonModQuizSyncProvider extends CoreCourseActivitySyncBaseProvider
*
* @param force Wether to force sync not depending on last execution.
* @param siteId Site ID to sync.
- * @returns Promise resolved if sync is successful, rejected if sync fails.
*/
protected async syncAllQuizzesFunc(force: boolean, siteId: string): Promise {
// Get all offline attempts.
diff --git a/src/addons/mod/quiz/services/quiz.ts b/src/addons/mod/quiz/services/quiz.ts
index 938081dbb52..0272fa7b036 100644
--- a/src/addons/mod/quiz/services/quiz.ts
+++ b/src/addons/mod/quiz/services/quiz.ts
@@ -50,6 +50,7 @@ import {
AddonModQuizGradeMethods,
AddonModQuizDisplayOptionsAttemptStates,
ADDON_MOD_QUIZ_IMMEDIATELY_AFTER_PERIOD,
+ AddonModQuizNavMethods,
} from '../constants';
import { CoreIonicColorNames } from '@singletons/colors';
import { CoreCacheUpdateFrequency } from '@/core/constants';
@@ -1577,7 +1578,7 @@ export class AddonModQuizProvider {
* @returns Whether navigation is sequential.
*/
isNavigationSequential(quiz: AddonModQuizQuizWSData): boolean {
- return quiz.navmethod == 'sequential';
+ return quiz.navmethod === AddonModQuizNavMethods.SEQ;
}
/**
@@ -2286,7 +2287,7 @@ export type AddonModQuizQuizWSData = {
reviewrightanswer?: number; // Whether users are allowed to review their quiz attempts at various times.
reviewoverallfeedback?: number; // Whether users are allowed to review their quiz attempts at various times.
questionsperpage?: number; // How often to insert a page break when editing the quiz, or when shuffling the question order.
- navmethod?: string; // Any constraints on how the user is allowed to navigate around the quiz.
+ navmethod?: AddonModQuizNavMethods; // Any constraints on how the user is allowed to navigate around the quiz.
shuffleanswers?: number; // Whether the parts of the question should be shuffled, in those question types that support it.
sumgrades?: number | null; // The total of all the question instance maxmarks.
grade?: number; // The total that the quiz overall grade is scaled to be out of.
diff --git a/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_27.png b/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_27.png
index 46c03674485..48e9e920d8d 100644
Binary files a/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_27.png and b/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_27.png differ
diff --git a/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_42.png b/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_42.png
index e62984bde1d..88010fb8461 100644
Binary files a/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_42.png and b/src/addons/mod/quiz/tests/behat/snapshots/attempt-a-quiz-in-app-submit-a-quiz--review-a-quiz-attempt_42.png differ
diff --git a/src/addons/mod/scorm/pages/player/player.html b/src/addons/mod/scorm/pages/player/player.html
index ba56e50d795..8ecff8877d0 100644
--- a/src/addons/mod/scorm/pages/player/player.html
+++ b/src/addons/mod/scorm/pages/player/player.html
@@ -12,7 +12,7 @@
-
+
diff --git a/src/addons/qbehaviour/deferredcbm/services/handlers/deferredcbm.ts b/src/addons/qbehaviour/deferredcbm/services/handlers/deferredcbm.ts
index f7eabd3ea02..03337eb1447 100644
--- a/src/addons/qbehaviour/deferredcbm/services/handlers/deferredcbm.ts
+++ b/src/addons/qbehaviour/deferredcbm/services/handlers/deferredcbm.ts
@@ -21,6 +21,7 @@ import { CoreQuestionQuestionParsed, CoreQuestionsAnswers, CoreQuestionState } f
import { CoreQuestionHelper } from '@features/question/services/question-helper';
import { AddonQbehaviourDeferredCBMComponent } from '../../component/deferredcbm';
import { CoreQuestionDelegate } from '@features/question/services/question-delegate';
+import { QuestionCompleteGradableResponse } from '@features/question/constants';
/**
* Handler to support deferred CBM question behaviour.
@@ -89,12 +90,12 @@ export class AddonQbehaviourDeferredCBMHandlerService implements CoreQuestionBeh
answers: CoreQuestionsAnswers,
component: string,
componentId: string | number,
- ): number {
+ ): QuestionCompleteGradableResponse {
// First check if the question answer is complete.
const complete = CoreQuestionDelegate.isCompleteResponse(question, answers, component, componentId);
if (complete > 0) {
// Answer is complete, check the user answered CBM too.
- return answers['-certainty'] ? 1 : 0;
+ return answers['-certainty'] ? QuestionCompleteGradableResponse.YES : QuestionCompleteGradableResponse.NO;
}
return complete;
diff --git a/src/addons/qbehaviour/deferredfeedback/services/handlers/deferredfeedback.ts b/src/addons/qbehaviour/deferredfeedback/services/handlers/deferredfeedback.ts
index c0cc4ab2364..477f1804673 100644
--- a/src/addons/qbehaviour/deferredfeedback/services/handlers/deferredfeedback.ts
+++ b/src/addons/qbehaviour/deferredfeedback/services/handlers/deferredfeedback.ts
@@ -13,6 +13,7 @@
// limitations under the License.
import { Injectable } from '@angular/core';
+import { QuestionCompleteGradableResponse } from '@features/question/constants';
import { CoreQuestionBehaviourHandler, CoreQuestionQuestionWithAnswers } from '@features/question/services/behaviour-delegate';
import { CoreQuestionDBRecord } from '@features/question/services/database/question';
@@ -134,7 +135,7 @@ export class AddonQbehaviourDeferredFeedbackHandlerService implements CoreQuesti
}
// Answers have changed. Now check if the response is complete and calculate the new state.
- let complete: number;
+ let complete: QuestionCompleteGradableResponse;
let newState: string;
if (isCompleteFn) {
@@ -190,7 +191,7 @@ export type isCompleteResponseFunction = (
answers: CoreQuestionsAnswers,
component: string,
componentId: string | number,
-) => number;
+) => QuestionCompleteGradableResponse;
/**
* Check if two responses are the same.
diff --git a/src/addons/qtype/calculated/calculated.module.ts b/src/addons/qtype/calculated/calculated.module.ts
index 73b7a5a3f76..75c77b89b92 100644
--- a/src/addons/qtype/calculated/calculated.module.ts
+++ b/src/addons/qtype/calculated/calculated.module.ts
@@ -14,18 +14,10 @@
import { APP_INITIALIZER, NgModule } from '@angular/core';
-import { CoreSharedModule } from '@/core/shared.module';
import { CoreQuestionDelegate } from '@features/question/services/question-delegate';
-import { AddonQtypeCalculatedComponent } from './component/calculated';
import { AddonQtypeCalculatedHandler } from './services/handlers/calculated';
@NgModule({
- declarations: [
- AddonQtypeCalculatedComponent,
- ],
- imports: [
- CoreSharedModule,
- ],
providers: [
{
provide: APP_INITIALIZER,
@@ -35,8 +27,5 @@ import { AddonQtypeCalculatedHandler } from './services/handlers/calculated';
},
},
],
- exports: [
- AddonQtypeCalculatedComponent,
- ],
})
export class AddonQtypeCalculatedModule {}
diff --git a/src/addons/qtype/calculated/services/handlers/calculated.ts b/src/addons/qtype/calculated/services/handlers/calculated.ts
index 316372f107f..aee236e113a 100644
--- a/src/addons/qtype/calculated/services/handlers/calculated.ts
+++ b/src/addons/qtype/calculated/services/handlers/calculated.ts
@@ -12,195 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Injectable, Type } from '@angular/core';
+import { Injectable } from '@angular/core';
+import { AddonQtypeNumericalHandlerService } from '@addons/qtype/numerical/services/handlers/numerical';
-import { CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question';
-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';
/**
* Handler to support calculated question type.
+ * This question type depends on numeric question type.
*/
@Injectable({ providedIn: 'root' })
-export class AddonQtypeCalculatedHandlerService implements CoreQuestionHandler {
-
- static readonly UNITINPUT = '0';
- static readonly UNITRADIO = '1';
- static readonly UNITSELECT = '2';
- static readonly UNITNONE = '3';
-
- static readonly UNITGRADED = '1';
- static readonly UNITOPTIONAL = '0';
+export class AddonQtypeCalculatedHandlerService extends AddonQtypeNumericalHandlerService {
name = 'AddonQtypeCalculated';
type = 'qtype_calculated';
- /**
- * @inheritdoc
- */
- async getComponent(): Promise> {
- const { AddonQtypeCalculatedComponent } = await import('../../component/calculated');
-
- return AddonQtypeCalculatedComponent;
- }
-
- /**
- * Check if the units are in a separate field for the question.
- *
- * @param question Question.
- * @returns Whether units are in a separate field.
- */
- hasSeparateUnitField(question: CoreQuestionQuestionParsed): boolean {
- if (!question.parsedSettings) {
- const element = convertTextToHTMLElement(question.html);
-
- return !!(element.querySelector('select[name*=unit]') || element.querySelector('input[type="radio"]'));
- }
-
- return question.parsedSettings.unitdisplay === AddonQtypeCalculatedHandlerService.UNITRADIO ||
- question.parsedSettings.unitdisplay === AddonQtypeCalculatedHandlerService.UNITSELECT;
- }
-
- /**
- * @inheritdoc
- */
- isCompleteResponse(
- question: CoreQuestionQuestionParsed,
- answers: CoreQuestionsAnswers,
- ): number {
- if (!this.isGradableResponse(question, answers)) {
- return 0;
- }
-
- const { answer, unit } = this.parseAnswer(question, answers.answer);
- if (answer === null) {
- return 0;
- }
-
- if (!question.parsedSettings) {
- if (this.hasSeparateUnitField(question)) {
- return this.isValidValue( answers.unit) ? 1 : 0;
- }
-
- // We cannot know if the answer should contain units or not.
- return -1;
- }
-
- if (question.parsedSettings.unitdisplay != AddonQtypeCalculatedHandlerService.UNITINPUT && unit) {
- // There should be no units or be outside of the input, not valid.
- return 0;
- }
-
- if (this.hasSeparateUnitField(question) && !this.isValidValue( answers.unit)) {
- // Unit not supplied as a separate field and it's required.
- return 0;
- }
-
- 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;
- }
-
- return 1;
- }
-
- /**
- * @inheritdoc
- */
- async isEnabled(): Promise {
- return true;
- }
-
- /**
- * @inheritdoc
- */
- isGradableResponse(
- question: CoreQuestionQuestionParsed,
- answers: CoreQuestionsAnswers,
- ): number {
- return this.isValidValue( answers.answer) ? 1 : 0;
- }
-
- /**
- * @inheritdoc
- */
- isSameResponse(
- question: CoreQuestionQuestionParsed,
- prevAnswers: CoreQuestionsAnswers,
- newAnswers: CoreQuestionsAnswers,
- ): boolean {
- return CoreObject.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer') &&
- CoreObject.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'unit');
- }
-
- /**
- * Check if a value is valid (not empty).
- *
- * @param value Value to check.
- * @returns Whether the value is valid.
- */
- isValidValue(value: string | number | null): boolean {
- return !!value || value === '0' || value === 0;
- }
-
- /**
- * Parse an answer string.
- *
- * @param question Question.
- * @param answer Answer.
- * @returns Answer and unit.
- */
- parseAnswer(question: CoreQuestionQuestionParsed, answer: string): { answer: number | null; unit: string | null } {
- if (!answer) {
- return { answer: null, unit: null };
- }
-
- let regexString = '[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[-+]?\\d+)?';
-
- // Strip spaces (which may be thousands separators) and change other forms of writing e to e.
- answer = answer.replace(/ /g, '');
- answer = answer.replace(/(?:e|E|(?:x|\*|×)10(?:\^|\*\*))([+-]?\d+)/, 'e$1');
-
- // 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) {
- answer = answer.replace(',', '');
- } else {
- answer = answer.replace(',', '.');
- }
-
- let unitsLeft = false;
- let match: RegExpMatchArray | null = null;
-
- if (!question.parsedSettings || question.parsedSettings.unitsleft === null) {
- // We don't know if units should be before or after so we check both.
- match = answer.match(new RegExp('^' + regexString));
- if (!match) {
- unitsLeft = true;
- match = answer.match(new RegExp(regexString + '$'));
- }
- } else {
- unitsLeft = question.parsedSettings.unitsleft == '1';
- regexString = unitsLeft ? regexString + '$' : '^' + regexString;
-
- match = answer.match(new RegExp(regexString));
- }
-
- if (!match) {
- return { answer: null, unit: null };
- }
-
- const numberString = match[0];
- const unit = unitsLeft ? answer.substring(0, answer.length - match[0].length) : answer.substring(match[0].length);
-
- // No need to calculate the multiplier.
- return { answer: Number(numberString), unit };
- }
-
}
export const AddonQtypeCalculatedHandler = makeSingleton(AddonQtypeCalculatedHandlerService);
diff --git a/src/addons/qtype/calculatedmulti/services/handlers/calculatedmulti.ts b/src/addons/qtype/calculatedmulti/services/handlers/calculatedmulti.ts
index 3d06667c31f..f436f232906 100644
--- a/src/addons/qtype/calculatedmulti/services/handlers/calculatedmulti.ts
+++ b/src/addons/qtype/calculatedmulti/services/handlers/calculatedmulti.ts
@@ -18,6 +18,7 @@ import { CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/ques
import { CoreQuestionHandler } from '@features/question/services/question-delegate';
import { makeSingleton } from '@singletons';
import { AddonQtypeMultichoiceHandler } from '@addons/qtype/multichoice/services/handlers/multichoice';
+import { QuestionCompleteGradableResponse } from '@features/question/constants';
/**
* Handler to support calculated multi question type.
@@ -44,7 +45,7 @@ export class AddonQtypeCalculatedMultiHandlerService implements CoreQuestionHand
isCompleteResponse(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
- ): number {
+ ): QuestionCompleteGradableResponse {
// This question type depends on multichoice.
return AddonQtypeMultichoiceHandler.isCompleteResponseSingle(answers);
}
@@ -62,7 +63,7 @@ export class AddonQtypeCalculatedMultiHandlerService implements CoreQuestionHand
isGradableResponse(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
- ): number {
+ ): QuestionCompleteGradableResponse {
// This question type depends on multichoice.
return AddonQtypeMultichoiceHandler.isGradableResponseSingle(answers);
}
@@ -79,6 +80,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);
diff --git a/src/addons/qtype/calculatedsimple/services/handlers/calculatedsimple.ts b/src/addons/qtype/calculatedsimple/services/handlers/calculatedsimple.ts
index 787e2d552cf..c7cc17de447 100644
--- a/src/addons/qtype/calculatedsimple/services/handlers/calculatedsimple.ts
+++ b/src/addons/qtype/calculatedsimple/services/handlers/calculatedsimple.ts
@@ -12,73 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Injectable, Type } from '@angular/core';
+import { Injectable } from '@angular/core';
-import { CoreQuestionHandler } from '@features/question/services/question-delegate';
-import { AddonQtypeCalculatedHandler } from '@addons/qtype/calculated/services/handlers/calculated';
-import { CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question';
import { makeSingleton } from '@singletons';
+import { AddonQtypeNumericalHandlerService } from '@addons/qtype/numerical/services/handlers/numerical';
/**
* Handler to support calculated simple question type.
+ * This question type depends on numeric question type.
*/
@Injectable({ providedIn: 'root' })
-export class AddonQtypeCalculatedSimpleHandlerService implements CoreQuestionHandler {
+export class AddonQtypeCalculatedSimpleHandlerService extends AddonQtypeNumericalHandlerService {
name = 'AddonQtypeCalculatedSimple';
type = 'qtype_calculatedsimple';
- /**
- * @inheritdoc
- */
- async getComponent(): Promise> {
- // Calculated simple behaves like a calculated, use the same component.
- const { AddonQtypeCalculatedComponent } = await import('@addons/qtype/calculated/component/calculated');
-
- return AddonQtypeCalculatedComponent;
- }
-
- /**
- * @inheritdoc
- */
- isCompleteResponse(
- question: CoreQuestionQuestionParsed,
- answers: CoreQuestionsAnswers,
- ): number {
- // This question type depends on calculated.
- return AddonQtypeCalculatedHandler.isCompleteResponse(question, answers);
- }
-
- /**
- * @inheritdoc
- */
- async isEnabled(): Promise {
- return true;
- }
-
- /**
- * @inheritdoc
- */
- isGradableResponse(
- question: CoreQuestionQuestionParsed,
- answers: CoreQuestionsAnswers,
- ): number {
- // This question type depends on calculated.
- return AddonQtypeCalculatedHandler.isGradableResponse(question, answers);
- }
-
- /**
- * @inheritdoc
- */
- isSameResponse(
- question: CoreQuestionQuestionParsed,
- prevAnswers: CoreQuestionsAnswers,
- newAnswers: CoreQuestionsAnswers,
- ): boolean {
- // This question type depends on calculated.
- return AddonQtypeCalculatedHandler.isSameResponse(question, prevAnswers, newAnswers);
- }
-
}
export const AddonQtypeCalculatedSimpleHandler = makeSingleton(AddonQtypeCalculatedSimpleHandlerService);
diff --git a/src/addons/qtype/ddimageortext/component/ddimageortext.ts b/src/addons/qtype/ddimageortext/component/ddimageortext.ts
index 2fbf0b74689..3c1ffacaa2c 100644
--- a/src/addons/qtype/ddimageortext/component/ddimageortext.ts
+++ b/src/addons/qtype/ddimageortext/component/ddimageortext.ts
@@ -45,11 +45,15 @@ export class AddonQtypeDdImageOrTextComponent
*/
init(): void {
if (!this.question) {
+ this.onReadyPromise.resolve();
+
return;
}
const questionElement = this.initComponent();
if (!questionElement) {
+ this.onReadyPromise.resolve();
+
return;
}
@@ -57,6 +61,7 @@ export class AddonQtypeDdImageOrTextComponent
const ddArea = questionElement.querySelector('.ddarea');
if (!ddArea) {
this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
+ this.onReadyPromise.resolve();
return CoreQuestionHelper.showComponentError(this.onAbort);
}
@@ -84,6 +89,7 @@ export class AddonQtypeDdImageOrTextComponent
}
this.question.loaded = false;
+ this.onReadyPromise.resolve();
}
/**
diff --git a/src/addons/qtype/ddimageortext/lang.json b/src/addons/qtype/ddimageortext/lang.json
new file mode 100644
index 00000000000..ca2e37d3604
--- /dev/null
+++ b/src/addons/qtype/ddimageortext/lang.json
@@ -0,0 +1,3 @@
+{
+ "pleasedraganimagetoeachdropregion": "Your answer is not complete; please drag an item to each drop region."
+}
diff --git a/src/addons/qtype/ddimageortext/services/handlers/ddimageortext.ts b/src/addons/qtype/ddimageortext/services/handlers/ddimageortext.ts
index 5729a9fe05e..22f2be6416c 100644
--- a/src/addons/qtype/ddimageortext/services/handlers/ddimageortext.ts
+++ b/src/addons/qtype/ddimageortext/services/handlers/ddimageortext.ts
@@ -13,10 +13,11 @@
// limitations under the License.
import { Injectable, Type } from '@angular/core';
+import { QuestionCompleteGradableResponse } from '@features/question/constants';
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.
@@ -53,17 +54,17 @@ export class AddonQtypeDdImageOrTextHandlerService implements CoreQuestionHandle
isCompleteResponse(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
- ): number {
+ ): QuestionCompleteGradableResponse {
// An answer is complete if all drop zones have an answer.
// We should always receive all the drop zones with their value ('' if not answered).
for (const name in answers) {
const value = answers[name];
if (!value || value === '0') {
- return 0;
+ return QuestionCompleteGradableResponse.NO;
}
}
- return 1;
+ return QuestionCompleteGradableResponse.YES;
}
/**
@@ -79,15 +80,15 @@ export class AddonQtypeDdImageOrTextHandlerService implements CoreQuestionHandle
isGradableResponse(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
- ): number {
+ ): QuestionCompleteGradableResponse{
for (const name in answers) {
const value = answers[name];
if (value && value !== '0') {
- return 1;
+ return QuestionCompleteGradableResponse.YES;
}
}
- return 0;
+ return QuestionCompleteGradableResponse.NO;
}
/**
@@ -101,6 +102,20 @@ export class AddonQtypeDdImageOrTextHandlerService implements CoreQuestionHandle
return CoreQuestion.compareAllAnswers(prevAnswers, newAnswers);
}
+ /**
+ * @inheritdoc
+ */
+ getValidationError(
+ question: CoreQuestionQuestionParsed,
+ answers: CoreQuestionsAnswers,
+ ): string | undefined {
+ if (this.isCompleteResponse(question, answers) === QuestionCompleteGradableResponse.YES) {
+ return;
+ }
+
+ return Translate.instant('addon.qtype_ddimageortext.pleasedraganimagetoeachdropregion');
+ }
+
}
export const AddonQtypeDdImageOrTextHandler = makeSingleton(AddonQtypeDdImageOrTextHandlerService);
diff --git a/src/addons/qtype/ddmarker/component/ddmarker.ts b/src/addons/qtype/ddmarker/component/ddmarker.ts
index e2cd2ebaa5e..a0026a62c5f 100644
--- a/src/addons/qtype/ddmarker/component/ddmarker.ts
+++ b/src/addons/qtype/ddmarker/component/ddmarker.ts
@@ -51,11 +51,15 @@ export class AddonQtypeDdMarkerComponent
*/
init(): void {
if (!this.question) {
+ this.onReadyPromise.resolve();
+
return;
}
const questionElement = this.initComponent();
if (!questionElement) {
+ this.onReadyPromise.resolve();
+
return;
}
@@ -65,6 +69,7 @@ export class AddonQtypeDdMarkerComponent
if (!ddArea || !ddForm) {
this.logger.warn('Aborting because of an error parsing question.', this.question.slot);
+ this.onReadyPromise.resolve();
return CoreQuestionHelper.showComponentError(this.onAbort);
}
@@ -107,6 +112,7 @@ export class AddonQtypeDdMarkerComponent
}
this.question.loaded = false;
+ this.onReadyPromise.resolve();
}
/**
diff --git a/src/addons/qtype/ddmarker/lang.json b/src/addons/qtype/ddmarker/lang.json
new file mode 100644
index 00000000000..06cf17456d2
--- /dev/null
+++ b/src/addons/qtype/ddmarker/lang.json
@@ -0,0 +1,3 @@
+{
+ "pleasedragatleastonemarker": "Your answer is not complete; you must place at least one marker on the image."
+}
diff --git a/src/addons/qtype/ddmarker/services/handlers/ddmarker.ts b/src/addons/qtype/ddmarker/services/handlers/ddmarker.ts
index 899e0619257..38754da8a62 100644
--- a/src/addons/qtype/ddmarker/services/handlers/ddmarker.ts
+++ b/src/addons/qtype/ddmarker/services/handlers/ddmarker.ts
@@ -13,12 +13,13 @@
// limitations under the License.
import { Injectable, Type } from '@angular/core';
+import { QuestionCompleteGradableResponse } from '@features/question/constants';
import { CoreQuestion, CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question';
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.
@@ -55,15 +56,15 @@ export class AddonQtypeDdMarkerHandlerService implements CoreQuestionHandler {
isCompleteResponse(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
- ): number {
+ ): QuestionCompleteGradableResponse {
// If 1 dragitem is set we assume the answer is complete (like Moodle does).
for (const name in answers) {
- if (answers[name]) {
- return 1;
+ if (name !== ':sequencecheck' && answers[name]) {
+ return QuestionCompleteGradableResponse.YES;
}
}
- return 0;
+ return QuestionCompleteGradableResponse.NO;
}
/**
@@ -79,7 +80,7 @@ export class AddonQtypeDdMarkerHandlerService implements CoreQuestionHandler {
isGradableResponse(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
- ): number {
+ ): QuestionCompleteGradableResponse {
return this.isCompleteResponse(question, answers);
}
@@ -102,7 +103,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],
@@ -112,6 +113,20 @@ export class AddonQtypeDdMarkerHandlerService implements CoreQuestionHandler {
return [];
}
+ /**
+ * @inheritdoc
+ */
+ getValidationError(
+ question: CoreQuestionQuestionParsed,
+ answers: CoreQuestionsAnswers,
+ ): string | undefined {
+ if (this.isCompleteResponse(question, answers) === QuestionCompleteGradableResponse.YES) {
+ return;
+ }
+
+ return Translate.instant('addon.qtype_ddmarker.pleasedragatleastonemarker');
+ }
+
}
export const AddonQtypeDdMarkerHandler = makeSingleton(AddonQtypeDdMarkerHandlerService);
diff --git a/src/addons/qtype/ddwtos/component/ddwtos.ts b/src/addons/qtype/ddwtos/component/ddwtos.ts
index eb9e11b5e73..fbf571eaca4 100644
--- a/src/addons/qtype/ddwtos/component/ddwtos.ts
+++ b/src/addons/qtype/ddwtos/component/ddwtos.ts
@@ -47,11 +47,15 @@ export class AddonQtypeDdwtosComponent extends CoreQuestionBaseComponent {
- 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);
diff --git a/src/addons/qtype/description/component/description.ts b/src/addons/qtype/description/component/description.ts
index f57f77f7f73..c50f2d0655e 100644
--- a/src/addons/qtype/description/component/description.ts
+++ b/src/addons/qtype/description/component/description.ts
@@ -36,12 +36,16 @@ export class AddonQtypeDescriptionComponent extends CoreQuestionBaseComponent {
init(): void {
const questionEl = this.initComponent();
if (!questionEl) {
+ this.onReadyPromise.resolve();
+
return;
}
// Get the "seen" hidden input.
const input = questionEl.querySelector('input[type="hidden"][name*=seen]');
if (!input) {
+ this.onReadyPromise.resolve();
+
return;
}
diff --git a/src/addons/qtype/essay/component/essay.ts b/src/addons/qtype/essay/component/essay.ts
index 8859c226455..216c1b3bb89 100644
--- a/src/addons/qtype/essay/component/essay.ts
+++ b/src/addons/qtype/essay/component/essay.ts
@@ -45,6 +45,8 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent answers.answer, onlineError);
}
@@ -195,34 +203,38 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
answers: CoreQuestionsAnswers,
component: string,
componentId: string | number,
- ): number {
+ ): QuestionCompleteGradableResponse {
const hasTextAnswer = !!answers.answer;
const uploadFilesSupported = question.responsefileareas !== undefined;
const allowedOptions = this.getAllowedOptions(question);
if (hasTextAnswer && this.checkInputWordCount(question, answers.answer, undefined)) {
- return 0;
+ return QuestionCompleteGradableResponse.NO;
}
if (!allowedOptions.attachments) {
- return hasTextAnswer ? 1 : 0;
+ return hasTextAnswer ? QuestionCompleteGradableResponse.YES : QuestionCompleteGradableResponse.NO;
}
if (!uploadFilesSupported || !question.parsedSettings) {
// We can't know if the attachments are required or if the user added any in web.
- return -1;
+ return QuestionCompleteGradableResponse.UNKNOWN;
}
const questionComponentId = CoreQuestion.getQuestionComponentId(question, componentId);
const attachments = CoreFileSession.getFiles(component, questionComponentId);
if (!allowedOptions.text) {
- return attachments && attachments.length >= Number(question.parsedSettings.attachmentsrequired) ? 1 : 0;
+ return attachments && attachments.length >= Number(question.parsedSettings.attachmentsrequired)
+ ? QuestionCompleteGradableResponse.YES
+ : QuestionCompleteGradableResponse.NO;
}
- return ((hasTextAnswer || question.parsedSettings.responserequired == '0') &&
- (attachments && attachments.length >= Number(question.parsedSettings.attachmentsrequired))) ? 1 : 0;
+ return ((hasTextAnswer || question.parsedSettings.responserequired === '0') &&
+ (attachments && attachments.length >= Number(question.parsedSettings.attachmentsrequired)))
+ ? QuestionCompleteGradableResponse.YES
+ : QuestionCompleteGradableResponse.NO;
}
/**
@@ -240,16 +252,18 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler {
answers: CoreQuestionsAnswers,
component: string,
componentId: string | number,
- ): number {
+ ): QuestionCompleteGradableResponse {
if (question.responsefileareas === undefined) {
- return -1;
+ return QuestionCompleteGradableResponse.UNKNOWN;
}
const questionComponentId = CoreQuestion.getQuestionComponentId(question, componentId);
const attachments = CoreFileSession.getFiles(component, questionComponentId);
// Determine if the given response has online text or attachments.
- return (answers.answer && answers.answer !== '') || (attachments && attachments.length > 0) ? 1 : 0;
+ return (answers.answer && answers.answer !== '') || (attachments && attachments.length > 0)
+ ? QuestionCompleteGradableResponse.YES
+ : QuestionCompleteGradableResponse.NO;
}
/**
@@ -452,8 +466,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');
diff --git a/src/addons/qtype/gapselect/component/gapselect.ts b/src/addons/qtype/gapselect/component/gapselect.ts
index 0c0852eb1f0..4b16d39661a 100644
--- a/src/addons/qtype/gapselect/component/gapselect.ts
+++ b/src/addons/qtype/gapselect/component/gapselect.ts
@@ -36,6 +36,7 @@ export class AddonQtypeGapSelectComponent extends CoreQuestionBaseComponent {
*/
init(): void {
this.initOriginalTextComponent('.qtext');
+ this.onReadyPromise.resolve();
}
/**
diff --git a/src/addons/qtype/gapselect/lang.json b/src/addons/qtype/gapselect/lang.json
new file mode 100644
index 00000000000..cb8e3bed9df
--- /dev/null
+++ b/src/addons/qtype/gapselect/lang.json
@@ -0,0 +1,3 @@
+{
+ "pleaseputananswerineachbox": "Please put an answer in each box."
+}
diff --git a/src/addons/qtype/gapselect/services/handlers/gapselect.ts b/src/addons/qtype/gapselect/services/handlers/gapselect.ts
index 3aa587ccd2c..bcb06a93f2c 100644
--- a/src/addons/qtype/gapselect/services/handlers/gapselect.ts
+++ b/src/addons/qtype/gapselect/services/handlers/gapselect.ts
@@ -13,10 +13,11 @@
// limitations under the License.
import { Injectable, Type } from '@angular/core';
+import { QuestionCompleteGradableResponse } from '@features/question/constants';
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.
@@ -53,16 +54,16 @@ export class AddonQtypeGapSelectHandlerService implements CoreQuestionHandler {
isCompleteResponse(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
- ): number {
+ ): QuestionCompleteGradableResponse {
// 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 || value === '0') {
- return 0;
+ return QuestionCompleteGradableResponse.NO;
}
}
- return 1;
+ return QuestionCompleteGradableResponse.YES;
}
/**
@@ -78,16 +79,16 @@ export class AddonQtypeGapSelectHandlerService implements CoreQuestionHandler {
isGradableResponse(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
- ): number {
+ ): QuestionCompleteGradableResponse {
// 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) {
- return 1;
+ if (value && value !== '0') {
+ return QuestionCompleteGradableResponse.YES;
}
}
- return 0;
+ return QuestionCompleteGradableResponse.NO;
}
/**
@@ -101,6 +102,20 @@ export class AddonQtypeGapSelectHandlerService implements CoreQuestionHandler {
return CoreQuestion.compareAllAnswers(prevAnswers, newAnswers);
}
+ /**
+ * @inheritdoc
+ */
+ getValidationError(
+ question: CoreQuestionQuestionParsed,
+ answers: CoreQuestionsAnswers,
+ ): string | undefined {
+ if (this.isCompleteResponse(question, answers) === QuestionCompleteGradableResponse.YES) {
+ return;
+ }
+
+ return Translate.instant('addon.qtype_gapselect.pleaseputananswerineachbox');
+ }
+
}
export const AddonQtypeGapSelectHandler = makeSingleton(AddonQtypeGapSelectHandlerService);
diff --git a/src/addons/qtype/match/component/match.ts b/src/addons/qtype/match/component/match.ts
index 7f0f5738395..98d6e383d42 100644
--- a/src/addons/qtype/match/component/match.ts
+++ b/src/addons/qtype/match/component/match.ts
@@ -35,6 +35,7 @@ export class AddonQtypeMatchComponent extends CoreQuestionBaseComponent(
CoreQuestionHelper.getAllInputNamesFromHtml(question.html || ''),
@@ -62,11 +63,11 @@ export class AddonQtypeMultiAnswerHandlerService implements CoreQuestionHandler
for (const name in names) {
const value = answers[name];
if (!value) {
- return 0;
+ return QuestionCompleteGradableResponse.NO;
}
}
- return 1;
+ return QuestionCompleteGradableResponse.YES;
}
/**
@@ -82,16 +83,16 @@ export class AddonQtypeMultiAnswerHandlerService implements CoreQuestionHandler
isGradableResponse(
question: CoreQuestionQuestionParsed,
answers: CoreQuestionsAnswers,
- ): number {
+ ): QuestionCompleteGradableResponse {
// 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 || value === false) {
- return 1;
+ return QuestionCompleteGradableResponse.YES;
}
}
- return 0;
+ return QuestionCompleteGradableResponse.NO;
}
/**
@@ -109,19 +110,34 @@ export class AddonQtypeMultiAnswerHandlerService implements CoreQuestionHandler
* @inheritdoc
*/
validateSequenceCheck(question: CoreQuestionQuestionParsed, offlineSequenceCheck: string): boolean {
- if (question.sequencecheck == Number(offlineSequenceCheck)) {
+ const offlineSequenceCheckNumber = Number(offlineSequenceCheck);
+ if (question.sequencecheck === offlineSequenceCheckNumber) {
return true;
}
// For some reason, viewing a multianswer for the first time without answering it creates a new step "todo".
// We'll treat this case as valid.
- if (question.sequencecheck == 2 && question.state == 'todo' && offlineSequenceCheck == '1') {
+ if (question.sequencecheck === 2 && question.state === 'todo' && offlineSequenceCheckNumber === 1) {
return true;
}
return false;
}
+ /**
+ * @inheritdoc
+ */
+ getValidationError(
+ question: CoreQuestionQuestionParsed,
+ answers: CoreQuestionsAnswers,
+ ): string | undefined {
+ if (this.isCompleteResponse(question, answers) === QuestionCompleteGradableResponse.YES) {
+ return;
+ }
+
+ return Translate.instant('addon.qtype_multianswer.pleaseananswerallparts');
+ }
+
}
export const AddonQtypeMultiAnswerHandler = makeSingleton(AddonQtypeMultiAnswerHandlerService);
diff --git a/src/addons/qtype/multichoice/component/multichoice.ts b/src/addons/qtype/multichoice/component/multichoice.ts
index 3a615401de3..4059d3d7387 100644
--- a/src/addons/qtype/multichoice/component/multichoice.ts
+++ b/src/addons/qtype/multichoice/component/multichoice.ts
@@ -35,6 +35,7 @@ export class AddonQtypeMultichoiceComponent extends CoreQuestionBaseComponent
+
{
+export class AddonQtypeNumericalComponent extends CoreQuestionBaseComponent {
constructor(elementRef: ElementRef) {
- super('AddonQtypeCalculatedComponent', elementRef);
+ super('AddonQtypeNumericalComponent', elementRef);
}
/**
@@ -35,6 +35,7 @@ export class AddonQtypeCalculatedComponent extends CoreQuestionBaseComponent> {
+ const { AddonQtypeNumericalComponent } = await import('../../component/numerical');
+
+ return AddonQtypeNumericalComponent;
+ }
+
+ /**
+ * Check if the units are in a separate field for the question.
+ *
+ * @param question Question.
+ * @returns Whether units are in a separate field.
+ */
+ protected hasSeparateUnitField(question: CoreQuestionQuestionParsed): boolean {
+ if (!question.parsedSettings) {
+ const element = convertTextToHTMLElement(question.html);
+
+ return !!(element.querySelector('select[name*=unit]') || element.querySelector('input[type="radio"]'));
+ }
+
+ return question.parsedSettings.unitdisplay === AddonQtypeNumericalHandlerService.UNITRADIO ||
+ question.parsedSettings.unitdisplay === AddonQtypeNumericalHandlerService.UNITSELECT;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ isCompleteResponse(
+ question: CoreQuestionQuestionParsed,
+ answers: CoreQuestionsAnswers,
+ ): QuestionCompleteGradableResponse {
+ if (!this.isGradableResponse(question, answers)) {
+ return QuestionCompleteGradableResponse.NO;
+ }
+
+ const { answer, unit } = this.parseAnswer(question, answers.answer);
+ if (answer === null) {
+ return QuestionCompleteGradableResponse.NO;
+ }
+
+ if (!question.parsedSettings) {
+ if (this.hasSeparateUnitField(question)) {
+ return this.isValidValue( answers.unit)
+ ? QuestionCompleteGradableResponse.YES
+ : QuestionCompleteGradableResponse.NO;
+ }
+
+ // We cannot know if the answer should contain units or not.
+ return QuestionCompleteGradableResponse.UNKNOWN;
+ }
+
+ if (question.parsedSettings.unitdisplay !== AddonQtypeNumericalHandlerService.UNITINPUT && unit) {
+ // There should be no units or be outside of the input, not valid.
+ return QuestionCompleteGradableResponse.NO;
+ }
+
+ if (this.hasSeparateUnitField(question) && !this.isValidValue( answers.unit)) {
+ // Unit not supplied as a separate field and it's required.
+ return QuestionCompleteGradableResponse.NO;
+ }
+
+ if (question.parsedSettings.unitdisplay === AddonQtypeNumericalHandlerService.UNITINPUT &&
+ question.parsedSettings.unitgradingtype === AddonQtypeNumericalHandlerService.UNITGRADED &&
+ !this.isValidValue(unit)) {
+ // Unit not supplied inside the input and it's required.
+ return QuestionCompleteGradableResponse.NO;
+ }
+
+ return QuestionCompleteGradableResponse.YES;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ async isEnabled(): Promise {
+ return true;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ isGradableResponse(
+ question: CoreQuestionQuestionParsed,
+ answers: CoreQuestionsAnswers,
+ ): QuestionCompleteGradableResponse {
+ return this.isValidValue( answers.answer)
+ ? QuestionCompleteGradableResponse.YES
+ : QuestionCompleteGradableResponse.NO;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ isSameResponse(
+ question: CoreQuestionQuestionParsed,
+ prevAnswers: CoreQuestionsAnswers,
+ newAnswers: CoreQuestionsAnswers,
+ ): boolean {
+ return CoreObject.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'answer') &&
+ CoreObject.sameAtKeyMissingIsBlank(prevAnswers, newAnswers, 'unit');
+ }
+
+ /**
+ * Check if a value is valid (not empty).
+ *
+ * @param value Value to check.
+ * @returns Whether the value is valid.
+ */
+ protected isValidValue(value: string | number | null): boolean {
+ return !!value || value === '0' || value === 0;
+ }
+
+ /**
+ * Parse an answer string.
+ *
+ * @param question Question.
+ * @param answer Answer.
+ * @returns Answer and unit.
+ */
+ protected parseAnswer(question: CoreQuestionQuestionParsed, answer: string): { answer: number | null; unit: string | null } {
+ if (!answer) {
+ return { answer: null, unit: null };
+ }
+
+ let regexString = '[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[-+]?\\d+)?';
+
+ // Strip spaces (which may be thousands separators) and change other forms of writing e to e.
+ answer = answer.replace(/ /g, '');
+ answer = answer.replace(/(?:e|E|(?:x|\*|×)10(?:\^|\*\*))([+-]?\d+)/, 'e$1');
+
+ // 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) {
+ answer = answer.replace(',', '');
+ } else {
+ answer = answer.replace(',', '.');
+ }
+
+ let unitsLeft = false;
+ let match: RegExpMatchArray | null = null;
+
+ if (!question.parsedSettings || question.parsedSettings.unitsleft === null) {
+ // We don't know if units should be before or after so we check both.
+ match = answer.match(new RegExp('^' + regexString));
+ if (!match) {
+ unitsLeft = true;
+ match = answer.match(new RegExp(regexString + '$'));
+ }
+ } else {
+ unitsLeft = question.parsedSettings.unitsleft === '1';
+ regexString = unitsLeft ? regexString + '$' : '^' + regexString;
+
+ match = answer.match(new RegExp(regexString));
+ }
+
+ if (!match) {
+ return { answer: null, unit: null };
+ }
+
+ const numberString = match[0];
+ const unit = unitsLeft ? answer.substring(0, answer.length - match[0].length) : answer.substring(match[0].length);
+
+ // No need to calculate the multiplier.
+ return { answer: Number(numberString), unit };
+ }
+
+ /**
+ * @inheritdoc
+ */
+ getValidationError(
+ question: CoreQuestionQuestionParsed,
+ answers: CoreQuestionsAnswers,
+ ): string | undefined {
+ if (this.isGradableResponse(question, answers) === QuestionCompleteGradableResponse.NO) {
+ return Translate.instant('addon.qtype_numerical.pleaseenterananswer');
+ }
+
+ const { answer, unit } = this.parseAnswer(question, answers.answer);
+ if (answer === null) {
+ return Translate.instant('addon.qtype_numerical.invalidnumber');
+ }
+
+ if (this.hasSeparateUnitField(question) && !this.isValidValue( answers.unit)) {
+ return Translate.instant('addon.qtype_numerical.unitnotselected');
+ }
+
+ if (!question.parsedSettings) {
+ // We cannot check anything else without settings.
+ return;
+ }
+
+ if (question.parsedSettings.unitdisplay !== AddonQtypeNumericalHandlerService.UNITINPUT && unit) {
+ // There should be no units or be outside of the input, not valid.
+ return Translate.instant('addon.qtype_numerical.invalidnumbernounit');
+ }
+
+ if (question.parsedSettings.unitdisplay === AddonQtypeNumericalHandlerService.UNITINPUT &&
+ question.parsedSettings.unitgradingtype === AddonQtypeNumericalHandlerService.UNITGRADED &&
+ !this.isValidValue(unit)) {
+ return Translate.instant('addon.qtype_numerical.invalidnumber');
+ }
+
+ return;
+ }
+
}
export const AddonQtypeNumericalHandler = makeSingleton(AddonQtypeNumericalHandlerService);
diff --git a/src/addons/qtype/ordering/component/ordering.ts b/src/addons/qtype/ordering/component/ordering.ts
index 41c2bdb9400..e2293b7ce37 100644
--- a/src/addons/qtype/ordering/component/ordering.ts
+++ b/src/addons/qtype/ordering/component/ordering.ts
@@ -48,11 +48,15 @@ export class AddonQtypeOrderingComponent extends CoreQuestionBaseComponent extends CorePromise {
* Reset status and value.
*/
reset(): void {
+ if (!this.isSettled()) {
+ return;
+ }
+
delete this.resolvedValue;
delete this.rejectedReason;
diff --git a/src/core/components/dynamic-component/dynamic-component.ts b/src/core/components/dynamic-component/dynamic-component.ts
index 52dbded55d1..d888aac5340 100644
--- a/src/core/components/dynamic-component/dynamic-component.ts
+++ b/src/core/components/dynamic-component/dynamic-component.ts
@@ -28,6 +28,8 @@ import {
KeyValueDiffer,
Type,
} from '@angular/core';
+import { AsyncDirective } from '@classes/async-directive';
+import { CorePromisedValue } from '@classes/promised-value';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreLogger } from '@singletons/logger';
@@ -63,7 +65,7 @@ import { CoreLogger } from '@singletons/logger';
templateUrl: 'core-dynamic-component.html',
styles: [':host { display: contents; }'],
})
-export class CoreDynamicComponent implements OnChanges, DoCheck {
+export class CoreDynamicComponent implements OnChanges, DoCheck, AsyncDirective {
@Input() component?: Type;
@Input() data?: Record;
@@ -77,13 +79,18 @@ export class CoreDynamicComponent implements OnChanges, DoCheck
setTimeout(() => this.createComponent());
}
- instance?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
+ protected promisedInstance = new CorePromisedValue(); // eslint-disable-line @typescript-eslint/no-explicit-any
+
container?: ViewContainerRef;
protected logger: CoreLogger;
protected differ: KeyValueDiffer; // To detect changes in the data input.
protected lastComponent?: Type;
+ get instance(): any { // eslint-disable-line @typescript-eslint/no-explicit-any
+ return this.promisedInstance.value;
+ }
+
constructor(
differs: KeyValueDiffers,
protected cdr: ChangeDetectorRef,
@@ -101,7 +108,7 @@ export class CoreDynamicComponent implements OnChanges, DoCheck
if (changes.component && !this.component) {
// Component not set, destroy the instance if any.
this.lastComponent = undefined;
- this.instance = undefined;
+ this.promisedInstance.reset();
this.container?.clear();
} else if (changes.component && (!this.instance || this.component != this.lastComponent)) {
this.createComponent();
@@ -112,18 +119,31 @@ export class CoreDynamicComponent implements OnChanges, DoCheck
* @inheritdoc
*/
ngDoCheck(): void {
- if (this.instance) {
- // Check if there's any change in the data object.
- const changes = this.differ.diff(this.data || {});
- if (changes) {
- this.setInputData();
- if (this.instance.ngOnChanges) {
- this.instance.ngOnChanges(CoreDomUtils.createChangesFromKeyValueDiff(changes));
- }
+ if (!this.instance) {
+ return;
+ }
+
+ // Check if there's any change in the data object.
+ const changes = this.differ.diff(this.data || {});
+ if (changes) {
+ this.setInputData();
+ if (this.instance.ngOnChanges) {
+ this.instance.ngOnChanges(CoreDomUtils.createChangesFromKeyValueDiff(changes));
}
}
}
+ /**
+ * @inheritdoc
+ */
+ async ready(): Promise {
+ const instance = await this.promisedInstance;
+
+ if (instance && typeof instance['ready'] === 'function') {
+ await instance.ready();
+ }
+ }
+
/**
* Call a certain method on the component.
*
@@ -135,11 +155,12 @@ export class CoreDynamicComponent implements OnChanges, DoCheck
method: Method,
...params: InstanceMethodParams
): InstanceMethodReturn | undefined {
- if (typeof this.instance?.[method] !== 'function') {
+ const instance = this.instance;
+ if (typeof instance?.[method] !== 'function') {
return;
}
- return this.instance[method].apply(this.instance, params);
+ return instance[method].apply(instance, params);
}
/**
@@ -155,7 +176,7 @@ export class CoreDynamicComponent implements OnChanges, DoCheck
return false;
}
- if (this.instance) {
+ if (this.promisedInstance.isSettled()) {
// Component already instantiated.
return true;
}
@@ -163,17 +184,18 @@ export class CoreDynamicComponent implements OnChanges, DoCheck
if (this.component instanceof ComponentRef) {
// A ComponentRef was supplied instead of the component class. Add it to the view.
this.container.insert(this.component.hostView);
- this.instance = this.component.instance;
// This feature is usually meant for site plugins. Inject some properties.
- this.instance['ChangeDetectorRef'] = this.cdr;
- this.instance['componentContainer'] = this.element.nativeElement;
+ this.component.instance['ChangeDetectorRef'] = this.cdr;
+ this.component.instance['componentContainer'] = this.element.nativeElement;
+
+ this.promisedInstance.resolve(this.component.instance);
} else {
try {
// Create the component and add it to the container.
const componentRef = this.container.createComponent(this.component);
- this.instance = componentRef.instance;
+ this.promisedInstance.resolve(componentRef.instance);
} catch (ex) {
this.logger.error('Error creating component', ex);
@@ -190,6 +212,10 @@ export class CoreDynamicComponent implements OnChanges, DoCheck
* Set the input data for the component.
*/
protected setInputData(): void {
+ if (!this.instance) {
+ return;
+ }
+
for (const name in this.data) {
this.instance[name] = this.data[name];
}
diff --git a/src/core/features/question/classes/base-question-component.ts b/src/core/features/question/classes/base-question-component.ts
index 91a55fdf144..ea4339f71ee 100644
--- a/src/core/features/question/classes/base-question-component.ts
+++ b/src/core/features/question/classes/base-question-component.ts
@@ -26,6 +26,8 @@ import { CoreQuestionBehaviourButton, CoreQuestionHelper, CoreQuestionQuestion }
import { ContextLevel } from '@/core/constants';
import { toBoolean } from '@/core/transforms/boolean';
import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
+import { CorePromisedValue } from '@classes/promised-value';
+import { AsyncDirective } from '@classes/async-directive';
/**
* Base class for components to render a question.
@@ -33,7 +35,7 @@ import { convertTextToHTMLElement } from '@/core/utils/create-html-element';
@Component({
template: '',
})
-export class CoreQuestionBaseComponent implements OnInit {
+export class CoreQuestionBaseComponent implements OnInit, AsyncDirective {
@Input() question?: T; // The question to render.
@Input() component?: string; // The component the question belongs to.
@@ -54,6 +56,7 @@ export class CoreQuestionBaseComponent();
constructor(@Optional() @Inject('') logName: string, elementRef: ElementRef) {
this.logger = CoreLogger.getInstance(logName);
@@ -82,6 +85,7 @@ export class CoreQuestionBaseComponent {
+ return this.onReadyPromise;
+ }
+
}
/**
* Any possible types of question.
*/
-export type AddonModQuizQuestion = AddonModQuizCalculatedQuestion | AddonModQuizEssayQuestion | AddonModQuizTextQuestion |
+export type AddonModQuizQuestion = AddonModQuizNumericalQuestion | AddonModQuizEssayQuestion | AddonModQuizTextQuestion |
AddonModQuizMatchQuestion | AddonModQuizMultichoiceQuestion;
/**
@@ -744,7 +755,7 @@ export type AddonModQuizQuestionBasicData = CoreQuestionQuestion & {
/**
* Data for calculated question.
*/
-export type AddonModQuizCalculatedQuestion = AddonModQuizTextQuestion & {
+export type AddonModQuizNumericalQuestion = AddonModQuizTextQuestion & {
select?: AddonModQuizQuestionSelect; // Select data if units use a select.
selectFirst?: boolean; // Whether the select is first or after the input.
options?: AddonModQuizQuestionRadioOption[]; // Options if units use radio buttons.
diff --git a/src/core/features/question/classes/base-question-handler.ts b/src/core/features/question/classes/base-question-handler.ts
index 3edcb57231d..9fdf587405f 100644
--- a/src/core/features/question/classes/base-question-handler.ts
+++ b/src/core/features/question/classes/base-question-handler.ts
@@ -16,6 +16,7 @@ import { Type } from '@angular/core';
import { CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '../services/question';
import { CoreQuestionHandler } from '../services/question-delegate';
+import { QuestionCompleteGradableResponse } from '../constants';
/**
* Base handler for question types.
@@ -100,8 +101,8 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler {
answers: CoreQuestionsAnswers, // eslint-disable-line @typescript-eslint/no-unused-vars
component: string, // eslint-disable-line @typescript-eslint/no-unused-vars
componentId: string | number, // eslint-disable-line @typescript-eslint/no-unused-vars
- ): number {
- return -1;
+ ): QuestionCompleteGradableResponse {
+ return QuestionCompleteGradableResponse.UNKNOWN;
}
/**
@@ -119,8 +120,8 @@ export class CoreQuestionBaseHandler implements CoreQuestionHandler {
answers: CoreQuestionsAnswers, // eslint-disable-line @typescript-eslint/no-unused-vars
component: string, // eslint-disable-line @typescript-eslint/no-unused-vars
componentId: string | number, // eslint-disable-line @typescript-eslint/no-unused-vars
- ): number {
- return -1;
+ ): QuestionCompleteGradableResponse {
+ return QuestionCompleteGradableResponse.UNKNOWN;
}
/**
diff --git a/src/core/features/question/components/question/core-question.html b/src/core/features/question/components/question/core-question.html
index 57ece7558a8..76d1d2c72a2 100644
--- a/src/core/features/question/components/question/core-question.html
+++ b/src/core/features/question/components/question/core-question.html
@@ -1,5 +1,5 @@
-
+
{{ 'core.question.errorquestionnotsupported' | translate:{$a: question?.type} }}
diff --git a/src/core/features/question/components/question/question.ts b/src/core/features/question/components/question/question.ts
index e1ac05885a1..49516e39a0f 100644
--- a/src/core/features/question/components/question/question.ts
+++ b/src/core/features/question/components/question/question.ts
@@ -14,8 +14,7 @@
import { ContextLevel } from '@/core/constants';
import { toBoolean } from '@/core/transforms/boolean';
-import { Component, Input, Output, OnInit, EventEmitter, ChangeDetectorRef, Type, ElementRef } from '@angular/core';
-import { AsyncDirective } from '@classes/async-directive';
+import { Component, Input, Output, OnInit, EventEmitter, ChangeDetectorRef, Type, ElementRef, ViewChild } from '@angular/core';
import { CorePromisedValue } from '@classes/promised-value';
import { CoreQuestionBehaviourDelegate } from '@features/question/services/behaviour-delegate';
import { CoreQuestionDelegate } from '@features/question/services/question-delegate';
@@ -26,6 +25,10 @@ import { CorePromiseUtils } from '@singletons/promise-utils';
import { Translate } from '@singletons';
import { CoreDirectivesRegistry } from '@singletons/directives-registry';
import { CoreLogger } from '@singletons/logger';
+import { CoreObject } from '@singletons/object';
+import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
+import { CoreQuestionBaseComponent } from '@features/question/classes/base-question-component';
+import { AsyncDirective } from '@classes/async-directive';
/**
* Component to render a question.
@@ -51,37 +54,53 @@ export class CoreQuestionComponent implements OnInit, AsyncDirective {
@Output() buttonClicked = new EventEmitter(); // Will emit when a behaviour button is clicked.
@Output() onAbort= new EventEmitter(); // Will emit an event if the question should be aborted.
+ @ViewChild(CoreDynamicComponent)
+ set dynComponent(el: CoreDynamicComponent) {
+ if (!el) {
+ return;
+ }
+
+ this.promisedDynamicComponent.resolve(el);
+ }
+
componentClass?: Type; // The class of the component to render.
data: Record = {}; // Data to pass to the component.
seqCheck?: { name: string; value: string }; // Sequenche check name and value (if any).
behaviourComponents?: Type[] = []; // Components to render the question behaviour.
- promisedReady: CorePromisedValue;
validationError?: string;
+ protected promisedDynamicComponent = new CorePromisedValue>();
+ protected showQuestionPromise = new CorePromisedValue();
+
protected logger: CoreLogger;
- get loaded(): boolean {
- return this.promisedReady.isResolved();
+ get showQuestion(): boolean {
+ return this.showQuestionPromise.isResolved();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ async ready(): Promise {
+ await this.showQuestionPromise;
+ const dynamicComponent = await this.promisedDynamicComponent;
+
+ return dynamicComponent.ready();
}
constructor(protected changeDetector: ChangeDetectorRef, private element: ElementRef) {
this.logger = CoreLogger.getInstance('CoreQuestionComponent');
- this.promisedReady = new CorePromisedValue();
CoreDirectivesRegistry.register(this.element.nativeElement, this);
}
- async ready(): Promise {
- await this.promisedReady;
- }
-
/**
* @inheritdoc
*/
async ngOnInit(): Promise {
- if (!this.question || (this.question.type != 'random' &&
+ if (!this.question || (this.question.type !== 'random' &&
!CoreQuestionDelegate.isQuestionSupported(this.question.type))) {
- this.promisedReady.resolve();
+ this.showQuestionPromise.resolve();
return;
}
@@ -92,10 +111,11 @@ export class CoreQuestionComponent implements OnInit, AsyncDirective {
);
if (!this.componentClass) {
- this.promisedReady.resolve();
+ this.showQuestionPromise.resolve();
return;
}
+
// Set up the data needed by the question and behaviour components.
this.data = {
question: this.question,
@@ -142,20 +162,9 @@ export class CoreQuestionComponent implements OnInit, AsyncDirective {
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 = {};
}
@@ -175,7 +184,36 @@ export class CoreQuestionComponent implements OnInit, AsyncDirective {
);
} finally {
this.question.html = CoreDomUtils.removeElementFromHtml(this.question.html, '.im-controls');
- this.promisedReady.resolve();
+ this.showQuestionPromise.resolve();
+ await this.loadValidationError();
+ }
+ }
+
+ /**
+ * Load the validation error of the question.
+ */
+ async loadValidationError(): Promise {
+ if (!this.question) {
+ return;
+ }
+
+ // Ensure question is completely initialized.
+ await this.ready();
+
+ // 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) {
+ if (this.question.localAnswers && !CoreObject.isEmpty(this.question.localAnswers)) {
+ this.validationError = CoreQuestionDelegate.getValidationError(
+ this.question,
+ this.question.localAnswers,
+ this.validationError,
+ this.component,
+ this.attemptId,
+ );
+ }
}
}
diff --git a/src/core/features/question/constants.ts b/src/core/features/question/constants.ts
index 11743566159..ea16b58484f 100644
--- a/src/core/features/question/constants.ts
+++ b/src/core/features/question/constants.ts
@@ -37,3 +37,12 @@ export const enum QuestionDisplayOptionsValues {
VISIBLE = 1,
EDITABLE = 2,
}
+
+/**
+ * Possible values for the question complete response or gradable response (compatible).
+ */
+export const enum QuestionCompleteGradableResponse {
+ UNKNOWN = -1,
+ NO = 0,
+ YES = 1,
+}
diff --git a/src/core/features/question/services/question-delegate.ts b/src/core/features/question/services/question-delegate.ts
index a1e374e5f0a..e041acc43b4 100644
--- a/src/core/features/question/services/question-delegate.ts
+++ b/src/core/features/question/services/question-delegate.ts
@@ -19,6 +19,7 @@ import { CoreWSFile } from '@services/ws';
import { makeSingleton } from '@singletons';
import { CoreQuestionDefaultHandler } from './handlers/default-question';
import { CoreQuestionQuestionParsed, CoreQuestionsAnswers } from './question';
+import { QuestionCompleteGradableResponse } from '../constants';
/**
* Interface that all question type handlers must implement.
@@ -59,6 +60,8 @@ export interface CoreQuestionHandler extends CoreDelegateHandler {
/**
* Check if there's a validation error with the offline data.
+ * In situations where isGradableResponse returns false, this method
+ * should generate a description of what the problem is.
*
* @param question The question.
* @param answers Object with the question offline answers (without prefix).
@@ -89,7 +92,7 @@ export interface CoreQuestionHandler extends CoreDelegateHandler {
answers: CoreQuestionsAnswers,
component: string,
componentId: string | number,
- ): number;
+ ): QuestionCompleteGradableResponse;
/**
* Check if a student has provided enough of an answer for the question to be graded automatically,
@@ -106,7 +109,7 @@ export interface CoreQuestionHandler extends CoreDelegateHandler {
answers: CoreQuestionsAnswers,
component: string,
componentId: string | number,
- ): number;
+ ): QuestionCompleteGradableResponse;
/**
* Check if two responses are the same.
@@ -294,7 +297,7 @@ export class CoreQuestionDelegateService extends CoreDelegate(
@@ -303,7 +306,7 @@ export class CoreQuestionDelegateService extends CoreDelegate(
@@ -330,7 +333,7 @@ export class CoreQuestionDelegateService extends CoreDelegate