From 3e1089adba376d81334cb6512fe80c41a82e0a4d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 22 Mar 2018 14:51:00 +0100 Subject: [PATCH] MOBILE-2348 quiz: Implement access rules delegate --- .../quiz/providers/access-rules-delegate.ts | 273 ++++++++++++++++++ src/addon/mod/quiz/quiz.module.ts | 27 ++ src/app/app.module.ts | 2 + src/classes/delegate.ts | 20 +- 4 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 src/addon/mod/quiz/providers/access-rules-delegate.ts create mode 100644 src/addon/mod/quiz/quiz.module.ts diff --git a/src/addon/mod/quiz/providers/access-rules-delegate.ts b/src/addon/mod/quiz/providers/access-rules-delegate.ts new file mode 100644 index 000000000..730023106 --- /dev/null +++ b/src/addon/mod/quiz/providers/access-rules-delegate.ts @@ -0,0 +1,273 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable, Injector } from '@angular/core'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; + +/** + * Interface that all access rules handlers must implement. + */ +export interface AddonModQuizAccessRuleHandler extends CoreDelegateHandler { + + /** + * Name of the rule the handler supports. E.g. 'password'. + * @type {string} + */ + ruleName: string; + + /** + * Whether the rule requires a preflight check when prefetch/start/continue an attempt. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} attempt The attempt started/continued. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {boolean|Promise} Whether the rule requires a preflight check. + */ + isPreflightCheckRequired(quiz: any, attempt: any, prefetch?: boolean, siteId?: string): boolean | Promise; + + /** + * Add preflight data that doesn't require user interaction. The data should be added to the preflightData param. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} attempt The attempt started/continued. + * @param {any} preflightData Object where to add the preflight data. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} Promise resolved when done if async, void if it's synchronous. + */ + getFixedPreflightData?(quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string): void | Promise; + + /** + * 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. + * + * @param {Injector} injector Injector. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getPreflightComponent?(injector: Injector): any | Promise; + + /** + * Function called when the preflight check has passed. This is a chance to record that fact in some way. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} attempt The attempt started/continued. + * @param {any} preflightData Preflight data gathered. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} Promise resolved when done if async, void if it's synchronous. + */ + notifyPreflightCheckPassed?(quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string) + : void | Promise; + + /** + * Function called when the preflight check fails. This is a chance to record that fact in some way. + * + * @param {any} quiz The quiz the rule belongs to. + * @param {any} attempt The attempt started/continued. + * @param {any} preflightData Preflight data gathered. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} Promise resolved when done if async, void if it's synchronous. + */ + notifyPreflightCheckFailed?(quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string) + : void | Promise; + + /** + * Whether or not the time left of an attempt should be displayed. + * + * @param {any} attempt The attempt. + * @param {number} endTime The attempt end time (in seconds). + * @param {number} timeNow The current time in seconds. + * @return {boolean} Whether it should be displayed. + */ + shouldShowTimeLeft?(attempt: any, endTime: number, timeNow: number): boolean; +} + +/** + * Delegate to register access rules for quiz module. + */ +@Injectable() +export class AddonModQuizAccessRuleDelegate extends CoreDelegate { + + protected handlerNameProperty = 'ruleName'; + + constructor(logger: CoreLoggerProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, + protected utils: CoreUtilsProvider) { + super('AddonModQuizAccessRulesDelegate', logger, sitesProvider, eventsProvider); + } + + /** + * Get the handler for a certain rule. + * + * @param {string} ruleName Name of the access rule. + * @return {AddonModQuizAccessRuleHandler} Handler. Undefined if no handler found for the rule. + */ + getAccessRuleHandler(ruleName: string): AddonModQuizAccessRuleHandler { + return this.getHandler(ruleName, true); + } + + /** + * Given a list of rules, get some fixed preflight data (data that doesn't require user interaction). + * + * @param {string[]} rules List of active rules names. + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} preflightData Object where to store the preflight data. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when all the data has been gathered. + */ + getFixedPreflightData(rules: string[], quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string) + : Promise { + rules = rules || []; + + const promises = []; + rules.forEach((rule) => { + promises.push(Promise.resolve( + this.executeFunctionOnEnabled(rule, 'getFixedPreflightData', [quiz, attempt, preflightData, prefetch, siteId]) + )); + }); + + return this.utils.allPromises(promises).catch(() => { + // Never reject. + }); + } + + /** + * Check if an access rule is supported. + * + * @param {string} ruleName Name of the rule. + * @return {boolean} Whether it's supported. + */ + isAccessRuleSupported(ruleName: string): boolean { + return this.hasHandler(ruleName, true); + } + + /** + * Given a list of rules, check if preflight check is required. + * + * @param {string[]} rules List of active rules names. + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: whether it's required. + */ + isPreflightCheckRequired(rules: string[], quiz: any, attempt: any, prefetch?: boolean, siteId?: string): Promise { + rules = rules || []; + + const promises = []; + let isRequired = false; + + rules.forEach((rule) => { + promises.push(Promise.resolve( + this.executeFunctionOnEnabled(rule, 'isPreflightCheckRequired', [quiz, attempt, prefetch, siteId]) + ).then((required) => { + if (required) { + isRequired = true; + } + })); + }); + + return this.utils.allPromises(promises).then(() => { + return isRequired; + }).catch(() => { + // Never reject. + return isRequired; + }); + } + + /** + * Notify all rules that the preflight check has passed. + * + * @param {string[]} rules List of active rules names. + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} preflightData Preflight data gathered. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + notifyPreflightCheckPassed(rules: string[], quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string) + : Promise { + rules = rules || []; + + const promises = []; + rules.forEach((rule) => { + promises.push(Promise.resolve( + this.executeFunctionOnEnabled(rule, 'notifyPreflightCheckPassed', [quiz, attempt, preflightData, prefetch, siteId]) + )); + }); + + return this.utils.allPromises(promises).catch(() => { + // Never reject. + }); + } + + /** + * Notify all rules that the preflight check has failed. + * + * @param {string[]} rules List of active rules names. + * @param {any} quiz Quiz. + * @param {any} attempt Attempt. + * @param {any} preflightData Preflight data gathered. + * @param {boolean} [prefetch] Whether the user is prefetching the quiz. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + notifyPreflightCheckFailed(rules: string[], quiz: any, attempt: any, preflightData: any, prefetch?: boolean, siteId?: string) + : Promise { + rules = rules || []; + + const promises = []; + rules.forEach((rule) => { + promises.push(Promise.resolve( + this.executeFunctionOnEnabled(rule, 'notifyPreflightCheckFailed', [quiz, attempt, preflightData, prefetch, siteId]) + )); + }); + + return this.utils.allPromises(promises).catch(() => { + // Never reject. + }); + } + + /** + * Whether or not the time left of an attempt should be displayed. + * + * @param {string[]} rules List of active rules names. + * @param {any} attempt The attempt. + * @param {number} endTime The attempt end time (in seconds). + * @param {number} timeNow The current time in seconds. + * @return {boolean} Whether it should be displayed. + */ + shouldShowTimeLeft(rules: string[], attempt: any, endTime: number, timeNow: number): boolean { + rules = rules || []; + + for (const i in rules) { + const rule = rules[i]; + + if (this.executeFunctionOnEnabled(rule, 'shouldShowTimeLeft', [attempt, endTime, timeNow])) { + return true; + } + } + + return false; + } +} diff --git a/src/addon/mod/quiz/quiz.module.ts b/src/addon/mod/quiz/quiz.module.ts new file mode 100644 index 000000000..5c1f1e6b3 --- /dev/null +++ b/src/addon/mod/quiz/quiz.module.ts @@ -0,0 +1,27 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { AddonModQuizAccessRuleDelegate } from './providers/access-rules-delegate'; + +@NgModule({ + declarations: [ + ], + imports: [ + ], + providers: [ + AddonModQuizAccessRuleDelegate + ] +}) +export class AddonModQuizModule { } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 03f660c6a..97fd0a3fc 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -80,6 +80,7 @@ import { AddonModLabelModule } from '@addon/mod/label/label.module'; import { AddonModResourceModule } from '@addon/mod/resource/resource.module'; import { AddonModFolderModule } from '@addon/mod/folder/folder.module'; import { AddonModPageModule } from '@addon/mod/page/page.module'; +import { AddonModQuizModule } from '@addon/mod/quiz/quiz.module'; import { AddonModUrlModule } from '@addon/mod/url/url.module'; import { AddonModSurveyModule } from '@addon/mod/survey/survey.module'; import { AddonMessagesModule } from '@addon/messages/messages.module'; @@ -169,6 +170,7 @@ export const CORE_PROVIDERS: any[] = [ AddonModResourceModule, AddonModFolderModule, AddonModPageModule, + AddonModQuizModule, AddonModUrlModule, AddonModSurveyModule, AddonMessagesModule, diff --git a/src/classes/delegate.ts b/src/classes/delegate.ts index 953de9ac0..412dad239 100644 --- a/src/classes/delegate.ts +++ b/src/classes/delegate.ts @@ -83,6 +83,12 @@ export class CoreDelegate { */ protected handlerNameProperty = 'name'; + /** + * Set of promises to update a handler, to prevent doing the same operation twice. + * @type {{[siteId: string]: {[name: string]: Promise}}} + */ + protected updatePromises: {[siteId: string]: {[name: string]: Promise}} = {}; + /** * Constructor of the Delegate. * @@ -215,6 +221,13 @@ export class CoreDelegate { currentSite = this.sitesProvider.getCurrentSite(); let promise; + if (this.updatePromises[siteId] && this.updatePromises[siteId][handler.name]) { + // There's already an update ongoing for this handler, return the promise. + return this.updatePromises[siteId][handler.name]; + } else if (!this.updatePromises[siteId]) { + this.updatePromises[siteId] = {}; + } + if (!this.sitesProvider.isLoggedIn()) { promise = Promise.reject(null); } else if (this.isFeatureDisabled(handler, currentSite)) { @@ -224,7 +237,7 @@ export class CoreDelegate { } // Checks if the handler is enabled. - return promise.catch(() => { + this.updatePromises[siteId][handler.name] = promise.catch(() => { return false; }).then((enabled: boolean) => { // Verify that this call is the last one that was started. @@ -236,7 +249,12 @@ export class CoreDelegate { delete this.enabledHandlers[handler[this.handlerNameProperty]]; } } + }).finally(() => { + // Update finished, delete the promise. + delete this.updatePromises[siteId][handler.name]; }); + + return this.updatePromises[siteId][handler.name]; } /**