From 157a2882d359812e2ebf48084a66494a7fca170f Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 7 May 2018 14:39:33 +0200 Subject: [PATCH 1/9] MOBILE-2376 siteplugins: Support message output handlers --- src/addon/messageoutput/providers/delegate.ts | 2 +- .../classes/message-output-handler.ts | 47 +++++++++++++++++++ src/core/siteplugins/providers/helper.ts | 39 ++++++++++++++- 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 src/core/siteplugins/classes/message-output-handler.ts diff --git a/src/addon/messageoutput/providers/delegate.ts b/src/addon/messageoutput/providers/delegate.ts index cd1f96698..36ef9f416 100644 --- a/src/addon/messageoutput/providers/delegate.ts +++ b/src/addon/messageoutput/providers/delegate.ts @@ -82,7 +82,7 @@ export interface AddonMessageOutputHandlerData { constructor(protected loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, protected eventsProvider: CoreEventsProvider) { - super('CoreSettingsDelegate', loggerProvider, sitesProvider, eventsProvider); + super('AddonMessageOutputDelegate', loggerProvider, sitesProvider, eventsProvider); } /** diff --git a/src/core/siteplugins/classes/message-output-handler.ts b/src/core/siteplugins/classes/message-output-handler.ts new file mode 100644 index 000000000..264008d64 --- /dev/null +++ b/src/core/siteplugins/classes/message-output-handler.ts @@ -0,0 +1,47 @@ +// (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 { AddonMessageOutputHandler, AddonMessageOutputHandlerData } from '@addon/messageoutput/providers/delegate'; +import { CoreSitePluginsBaseHandler } from './base-handler'; + +/** + * Handler to display a message output settings option. + */ +export class CoreSitePluginsMessageOutputHandler extends CoreSitePluginsBaseHandler implements AddonMessageOutputHandler { + + constructor(name: string, public processorName: string, protected title: string, protected plugin: any, + protected handlerSchema: any, protected initResult: any) { + super(name); + } + + /** + * Returns the data needed to render the handler. + * + * @return {AddonMessageOutputHandlerData} Data. + */ + getDisplayData(): AddonMessageOutputHandlerData { + return { + priority: this.handlerSchema.priority, + label: this.title, + icon: this.handlerSchema.displaydata.icon, + page: 'CoreSitePluginsPluginPage', + pageParams: { + title: this.title, + component: this.plugin.component, + method: this.handlerSchema.method, + initResult: this.initResult + } + }; + } +} diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index ba2915e56..1e94fc03f 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -34,6 +34,7 @@ import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delega import { CoreCourseFormatDelegate } from '@core/course/providers/format-delegate'; import { CoreUserDelegate } from '@core/user/providers/user-delegate'; import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; +import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/delegate'; // Handler classes. import { CoreSitePluginsCourseFormatHandler } from '../classes/course-format-handler'; @@ -43,6 +44,7 @@ import { CoreSitePluginsModulePrefetchHandler } from '../classes/module-prefetch import { CoreSitePluginsMainMenuHandler } from '../classes/main-menu-handler'; import { CoreSitePluginsUserProfileHandler } from '../classes/user-handler'; import { CoreSitePluginsUserProfileFieldHandler } from '../classes/user-profile-field-handler'; +import { CoreSitePluginsMessageOutputHandler } from '../classes/message-output-handler'; /** * Helper service to provide functionalities regarding site plugins. It basically has the features to load and register site @@ -64,7 +66,8 @@ export class CoreSitePluginsHelperProvider { private compileProvider: CoreCompileProvider, private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private courseOptionsDelegate: CoreCourseOptionsDelegate, eventsProvider: CoreEventsProvider, private courseFormatDelegate: CoreCourseFormatDelegate, private profileFieldDelegate: CoreUserProfileFieldDelegate, - private textUtils: CoreTextUtilsProvider, private filepoolProvider: CoreFilepoolProvider) { + private textUtils: CoreTextUtilsProvider, private filepoolProvider: CoreFilepoolProvider, + private messageOutputDelegate: AddonMessageOutputDelegate) { this.logger = logger.getInstance('CoreSitePluginsHelperProvider'); // Fetch the plugins on login. @@ -433,6 +436,10 @@ export class CoreSitePluginsHelperProvider { promise = Promise.resolve(this.registerUserProfileFieldHandler(plugin, handlerName, handlerSchema, result)); break; + case 'AddonMessageOutputHandler': + promise = Promise.resolve(this.registerMessageOutputHandler(plugin, handlerName, handlerSchema, result)); + break; + default: // Nothing to do. promise = Promise.resolve(); @@ -532,6 +539,36 @@ export class CoreSitePluginsHelperProvider { return uniqueName; } + /** + * Given a handler in an plugin, register it in the main menu delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} initResult Result of the init WS call. + * @return {string} A string to identify the handler. + */ + protected registerMessageOutputHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any): string { + if (!handlerSchema.displaydata) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide displaydata', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in message output delegate:', plugin, handlerSchema, initResult); + + // Create and register the handler. + const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title), + processorName = plugin.component.replace('message_', ''); + + this.messageOutputDelegate.registerHandler(new CoreSitePluginsMessageOutputHandler(uniqueName, processorName, + prefixedTitle, plugin, handlerSchema, initResult)); + + return uniqueName; + } + /** * Given a handler in an plugin, register it in the module delegate. * From 3c57fb364fe2bf5115f6081f71418fc11dd25f77 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 7 May 2018 15:18:54 +0200 Subject: [PATCH 2/9] MOBILE-2376 siteplugins: Support settings handlers --- .../siteplugins/classes/settings-handler.ts | 50 +++++++++++++++++++ src/core/siteplugins/providers/helper.ts | 41 +++++++++++++-- 2 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 src/core/siteplugins/classes/settings-handler.ts diff --git a/src/core/siteplugins/classes/settings-handler.ts b/src/core/siteplugins/classes/settings-handler.ts new file mode 100644 index 000000000..a90764c8b --- /dev/null +++ b/src/core/siteplugins/classes/settings-handler.ts @@ -0,0 +1,50 @@ +// (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 { CoreSettingsHandler, CoreSettingsHandlerData } from '@core/settings/providers/delegate'; +import { CoreSitePluginsBaseHandler } from './base-handler'; + +/** + * Handler to display a site plugin in the settings. + */ +export class CoreSitePluginsSettingsHandler extends CoreSitePluginsBaseHandler implements CoreSettingsHandler { + priority: number; + + constructor(name: string, protected title: string, protected plugin: any, protected handlerSchema: any, + protected initResult: any) { + super(name); + + this.priority = handlerSchema.priority; + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreSettingsHandlerData} Data. + */ + getDisplayData(): CoreSettingsHandlerData { + return { + title: this.title, + icon: this.handlerSchema.displaydata.icon, + class: this.handlerSchema.displaydata.class, + page: 'CoreSitePluginsPluginPage', + params: { + title: this.title, + component: this.plugin.component, + method: this.handlerSchema.method, + initResult: this.initResult + } + }; + } +} diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index 1e94fc03f..cdd034b58 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -34,6 +34,7 @@ import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delega import { CoreCourseFormatDelegate } from '@core/course/providers/format-delegate'; import { CoreUserDelegate } from '@core/user/providers/user-delegate'; import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; +import { CoreSettingsDelegate } from '@core/settings/providers/delegate'; import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/delegate'; // Handler classes. @@ -44,6 +45,7 @@ import { CoreSitePluginsModulePrefetchHandler } from '../classes/module-prefetch import { CoreSitePluginsMainMenuHandler } from '../classes/main-menu-handler'; import { CoreSitePluginsUserProfileHandler } from '../classes/user-handler'; import { CoreSitePluginsUserProfileFieldHandler } from '../classes/user-profile-field-handler'; +import { CoreSitePluginsSettingsHandler } from '../classes/settings-handler'; import { CoreSitePluginsMessageOutputHandler } from '../classes/message-output-handler'; /** @@ -67,7 +69,7 @@ export class CoreSitePluginsHelperProvider { private courseOptionsDelegate: CoreCourseOptionsDelegate, eventsProvider: CoreEventsProvider, private courseFormatDelegate: CoreCourseFormatDelegate, private profileFieldDelegate: CoreUserProfileFieldDelegate, private textUtils: CoreTextUtilsProvider, private filepoolProvider: CoreFilepoolProvider, - private messageOutputDelegate: AddonMessageOutputDelegate) { + private settingsDelegate: CoreSettingsDelegate, private messageOutputDelegate: AddonMessageOutputDelegate) { this.logger = logger.getInstance('CoreSitePluginsHelperProvider'); // Fetch the plugins on login. @@ -436,7 +438,11 @@ export class CoreSitePluginsHelperProvider { promise = Promise.resolve(this.registerUserProfileFieldHandler(plugin, handlerName, handlerSchema, result)); break; - case 'AddonMessageOutputHandler': + case 'CoreSettingsDelegate': + promise = Promise.resolve(this.registerSettingsHandler(plugin, handlerName, handlerSchema, result)); + break; + + case 'AddonMessageOutputDelegate': promise = Promise.resolve(this.registerMessageOutputHandler(plugin, handlerName, handlerSchema, result)); break; @@ -540,7 +546,7 @@ export class CoreSitePluginsHelperProvider { } /** - * Given a handler in an plugin, register it in the main menu delegate. + * Given a handler in an plugin, register it in the message output delegate. * * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. @@ -603,6 +609,35 @@ export class CoreSitePluginsHelperProvider { return modName; } + /** + * Given a handler in an plugin, register it in the settings delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} initResult Result of the init WS call. + * @return {string} A string to identify the handler. + */ + protected registerSettingsHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any): string { + if (!handlerSchema.displaydata) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide displaydata', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in settings delegate:', plugin, handlerSchema, initResult); + + // Create and register the handler. + const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), + prefixedTitle = this.getPrefixedString(plugin.addon, handlerSchema.displaydata.title); + + this.settingsDelegate.registerHandler( + new CoreSitePluginsSettingsHandler(uniqueName, prefixedTitle, plugin, handlerSchema, initResult)); + + return uniqueName; + } + /** * Given a handler in an plugin, register it in the user profile delegate. * From 02fd27d3d43b5a4275a48cc63f075681e0adba1b Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 8 May 2018 13:18:37 +0200 Subject: [PATCH 3/9] MOBILE-2376 siteplugins: Support question types --- .../deferredcbm/component/deferredcbm.ts | 6 +- .../component/informationitem.ts | 6 +- .../components/compile-html/compile-html.ts | 12 +-- .../classes/base-question-component.ts | 6 +- .../siteplugins/classes/question-handler.ts | 39 ++++++++ .../components/components.module.ts | 10 ++- .../components/question/question.html | 1 + .../components/question/question.ts | 90 +++++++++++++++++++ src/core/siteplugins/providers/helper.ts | 61 ++++++++++++- 9 files changed, 211 insertions(+), 20 deletions(-) create mode 100644 src/core/siteplugins/classes/question-handler.ts create mode 100644 src/core/siteplugins/components/question/question.html create mode 100644 src/core/siteplugins/components/question/question.ts diff --git a/src/addon/qbehaviour/deferredcbm/component/deferredcbm.ts b/src/addon/qbehaviour/deferredcbm/component/deferredcbm.ts index a6c822770..ad204e3a0 100644 --- a/src/addon/qbehaviour/deferredcbm/component/deferredcbm.ts +++ b/src/addon/qbehaviour/deferredcbm/component/deferredcbm.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, EventEmitter } from '@angular/core'; +import { Component, Input, Output, EventEmitter } from '@angular/core'; /** * Component to render the deferred CBM in a question. @@ -27,8 +27,8 @@ export class AddonQbehaviourDeferredCBMComponent { @Input() componentId: number; // ID of the component the question belongs to. @Input() attemptId: number; // Attempt ID. @Input() offlineEnabled?: boolean | string; // Whether the question can be answered in offline. - @Input() buttonClicked: EventEmitter; // Should emit an event when a behaviour button is clicked. - @Input() onAbort: EventEmitter; // Should emit an event if the question should be aborted. + @Output() buttonClicked: EventEmitter; // Should emit an event when a behaviour button is clicked. + @Output() onAbort: EventEmitter; // Should emit an event if the question should be aborted. constructor() { // Nothing to do. diff --git a/src/addon/qbehaviour/informationitem/component/informationitem.ts b/src/addon/qbehaviour/informationitem/component/informationitem.ts index 35e37f3f3..e29339cff 100644 --- a/src/addon/qbehaviour/informationitem/component/informationitem.ts +++ b/src/addon/qbehaviour/informationitem/component/informationitem.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, EventEmitter } from '@angular/core'; +import { Component, Input, Output, EventEmitter } from '@angular/core'; /** * Component to render a "seen" hidden input for informationitem question behaviour. @@ -27,8 +27,8 @@ export class AddonQbehaviourInformationItemComponent { @Input() componentId: number; // ID of the component the question belongs to. @Input() attemptId: number; // Attempt ID. @Input() offlineEnabled?: boolean | string; // Whether the question can be answered in offline. - @Input() buttonClicked: EventEmitter; // Should emit an event when a behaviour button is clicked. - @Input() onAbort: EventEmitter; // Should emit an event if the question should be aborted. + @Output() buttonClicked: EventEmitter; // Should emit an event when a behaviour button is clicked. + @Output() onAbort: EventEmitter; // Should emit an event if the question should be aborted. constructor() { // Nothing to do. diff --git a/src/core/compile/components/compile-html/compile-html.ts b/src/core/compile/components/compile-html/compile-html.ts index fcbd23171..16ca3b26f 100644 --- a/src/core/compile/components/compile-html/compile-html.ts +++ b/src/core/compile/components/compile-html/compile-html.ts @@ -42,7 +42,7 @@ import { BehaviorSubject } from 'rxjs'; export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { @Input() text: string; // The HTML text to display. @Input() javascript: string; // The Javascript to execute in the component. - @Input() jsData; // Data to pass to the fake component. + @Input() jsData: any; // Data to pass to the fake component. // Get the container where to put the content. @ViewChild('dynamicComponent', { read: ViewContainerRef }) container: ViewContainerRef; @@ -98,13 +98,13 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { // If there is some javascript to run, prepare the instance. if (compileInstance.javascript) { compileInstance.compileProvider.injectLibraries(this); - - // Add some more components and classes. - this['ChangeDetectorRef'] = compileInstance.cdr; - this['NavController'] = compileInstance.navCtrl; - this['componentContainer'] = compileInstance.element; } + // Always add these elements, they could be needed on component init (componentObservable). + this['ChangeDetectorRef'] = compileInstance.cdr; + this['NavController'] = compileInstance.navCtrl; + this['componentContainer'] = compileInstance.element; + // Add the data passed to the component. for (const name in compileInstance.jsData) { this[name] = compileInstance.jsData[name]; diff --git a/src/core/question/classes/base-question-component.ts b/src/core/question/classes/base-question-component.ts index c86b7abe4..8b79eb23b 100644 --- a/src/core/question/classes/base-question-component.ts +++ b/src/core/question/classes/base-question-component.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Input, EventEmitter, Injector } from '@angular/core'; +import { Input, Output, EventEmitter, Injector } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; @@ -27,8 +27,8 @@ export class CoreQuestionBaseComponent { @Input() componentId: number; // ID of the component the question belongs to. @Input() attemptId: number; // Attempt ID. @Input() offlineEnabled?: boolean | string; // Whether the question can be answered in offline. - @Input() buttonClicked: EventEmitter; // Should emit an event when a behaviour button is clicked. - @Input() onAbort: EventEmitter; // Should emit an event if the question should be aborted. + @Output() buttonClicked: EventEmitter; // Should emit an event when a behaviour button is clicked. + @Output() onAbort: EventEmitter; // Should emit an event if the question should be aborted. protected logger; protected questionHelper: CoreQuestionHelperProvider; diff --git a/src/core/siteplugins/classes/question-handler.ts b/src/core/siteplugins/classes/question-handler.ts new file mode 100644 index 000000000..4950468ca --- /dev/null +++ b/src/core/siteplugins/classes/question-handler.ts @@ -0,0 +1,39 @@ +// (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 { Injector } from '@angular/core'; +import { CoreQuestionDefaultHandler } from '@core/question/providers/default-question-handler'; +import { CoreSitePluginsQuestionComponent } from '../components/question/question'; + +/** + * Handler to display a question site plugin. + */ +export class CoreSitePluginsQuestionHandler extends CoreQuestionDefaultHandler { + + constructor(public name: string, public type: string) { + super(); + } + + /** + * Return the Component to use to display the question. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} question The question to render. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector): any | Promise { + return CoreSitePluginsQuestionComponent; + } +} diff --git a/src/core/siteplugins/components/components.module.ts b/src/core/siteplugins/components/components.module.ts index 53a3d912c..4c809ca39 100644 --- a/src/core/siteplugins/components/components.module.ts +++ b/src/core/siteplugins/components/components.module.ts @@ -23,6 +23,7 @@ import { CoreSitePluginsModuleIndexComponent } from './module-index/module-index import { CoreSitePluginsCourseOptionComponent } from './course-option/course-option'; import { CoreSitePluginsCourseFormatComponent } from './course-format/course-format'; import { CoreSitePluginsUserProfileFieldComponent } from './user-profile-field/user-profile-field'; +import { CoreSitePluginsQuestionComponent } from './question/question'; @NgModule({ declarations: [ @@ -30,7 +31,8 @@ import { CoreSitePluginsUserProfileFieldComponent } from './user-profile-field/u CoreSitePluginsModuleIndexComponent, CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, - CoreSitePluginsUserProfileFieldComponent + CoreSitePluginsUserProfileFieldComponent, + CoreSitePluginsQuestionComponent ], imports: [ CommonModule, @@ -46,13 +48,15 @@ import { CoreSitePluginsUserProfileFieldComponent } from './user-profile-field/u CoreSitePluginsModuleIndexComponent, CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, - CoreSitePluginsUserProfileFieldComponent + CoreSitePluginsUserProfileFieldComponent, + CoreSitePluginsQuestionComponent ], entryComponents: [ CoreSitePluginsModuleIndexComponent, CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, - CoreSitePluginsUserProfileFieldComponent + CoreSitePluginsUserProfileFieldComponent, + CoreSitePluginsQuestionComponent ] }) export class CoreSitePluginsComponentsModule {} diff --git a/src/core/siteplugins/components/question/question.html b/src/core/siteplugins/components/question/question.html new file mode 100644 index 000000000..fec5e4726 --- /dev/null +++ b/src/core/siteplugins/components/question/question.html @@ -0,0 +1 @@ + diff --git a/src/core/siteplugins/components/question/question.ts b/src/core/siteplugins/components/question/question.ts new file mode 100644 index 000000000..f7e46cc52 --- /dev/null +++ b/src/core/siteplugins/components/question/question.ts @@ -0,0 +1,90 @@ +// (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 { Component, OnInit, Input, Output, EventEmitter, ViewChild, OnDestroy } from '@angular/core'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; +import { CoreCompileHtmlComponent } from '@core/compile/components/compile-html/compile-html'; +import { Subscription } from 'rxjs'; + +/** + * Component that displays a question created using a site plugin. + */ +@Component({ + selector: 'core-site-plugins-question', + templateUrl: 'question.html', +}) +export class CoreSitePluginsQuestionComponent implements OnInit, OnDestroy { + @Input() question: any; // The question to render. + @Input() component: string; // The component the question belongs to. + @Input() componentId: number; // ID of the component the question belongs to. + @Input() attemptId: number; // Attempt ID. + @Input() offlineEnabled?: boolean | string; // Whether the question can be answered in offline. + @Output() buttonClicked: EventEmitter; // Should emit an event when a behaviour button is clicked. + @Output() onAbort: EventEmitter; // Should emit an event if the question should be aborted. + + @ViewChild(CoreCompileHtmlComponent) compileComponent: CoreCompileHtmlComponent; + + content = ''; // Content. + jsData; + protected componentObserver: Subscription; + + constructor(protected sitePluginsProvider: CoreSitePluginsProvider) { } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Pass the input and output data to the component. + this.jsData = { + question: this.question, + component: this.component, + componentId: this.componentId, + attemptId: this.attemptId, + offlineEnabled: this.offlineEnabled, + buttonClicked: this.buttonClicked, + onAbort: this.onAbort + }; + + if (this.question) { + // Retrieve the handler data. + const handler = this.sitePluginsProvider.getSitePluginHandler('qtype_' + this.question.type), + handlerSchema = handler && handler.handlerSchema; + + if (handlerSchema) { + // Load first template. + if (handlerSchema.methodTemplates && handlerSchema.methodTemplates.length) { + this.content = handler.handlerSchema.methodTemplates[0].html; + } + + // Wait for the instance to be created. + if (this.compileComponent && this.compileComponent.componentObservable && + handlerSchema.methodJSResult && handlerSchema.methodJSResult.componentInit) { + this.componentObserver = this.compileComponent.componentObservable.subscribe((instance) => { + if (instance) { + // Instance created, call component init. + handlerSchema.methodJSResult.componentInit.apply(instance); + } + }); + } + } + } + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.componentObserver && this.componentObserver.unsubscribe(); + } +} diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index cdd034b58..cdd570dd9 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -35,6 +35,7 @@ import { CoreCourseFormatDelegate } from '@core/course/providers/format-delegate import { CoreUserDelegate } from '@core/user/providers/user-delegate'; import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; import { CoreSettingsDelegate } from '@core/settings/providers/delegate'; +import { CoreQuestionDelegate } from '@core/question/providers/delegate'; import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/delegate'; // Handler classes. @@ -46,6 +47,7 @@ import { CoreSitePluginsMainMenuHandler } from '../classes/main-menu-handler'; import { CoreSitePluginsUserProfileHandler } from '../classes/user-handler'; import { CoreSitePluginsUserProfileFieldHandler } from '../classes/user-profile-field-handler'; import { CoreSitePluginsSettingsHandler } from '../classes/settings-handler'; +import { CoreSitePluginsQuestionHandler } from '../classes/question-handler'; import { CoreSitePluginsMessageOutputHandler } from '../classes/message-output-handler'; /** @@ -69,7 +71,9 @@ export class CoreSitePluginsHelperProvider { private courseOptionsDelegate: CoreCourseOptionsDelegate, eventsProvider: CoreEventsProvider, private courseFormatDelegate: CoreCourseFormatDelegate, private profileFieldDelegate: CoreUserProfileFieldDelegate, private textUtils: CoreTextUtilsProvider, private filepoolProvider: CoreFilepoolProvider, - private settingsDelegate: CoreSettingsDelegate, private messageOutputDelegate: AddonMessageOutputDelegate) { + private settingsDelegate: CoreSettingsDelegate, private questionDelegate: CoreQuestionDelegate, + private messageOutputDelegate: AddonMessageOutputDelegate) { + this.logger = logger.getInstance('CoreSitePluginsHelperProvider'); // Fetch the plugins on login. @@ -442,6 +446,10 @@ export class CoreSitePluginsHelperProvider { promise = Promise.resolve(this.registerSettingsHandler(plugin, handlerName, handlerSchema, result)); break; + case 'CoreQuestionDelegate': + promise = Promise.resolve(this.registerQuestionHandler(plugin, handlerName, handlerSchema, result)); + break; + case 'AddonMessageOutputDelegate': promise = Promise.resolve(this.registerMessageOutputHandler(plugin, handlerName, handlerSchema, result)); break; @@ -609,6 +617,55 @@ export class CoreSitePluginsHelperProvider { return modName; } + /** + * Given a handler in an plugin, register it in the question delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} initResult Result of the init WS call. + * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. + */ + protected registerQuestionHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any) + : string | Promise { + if (!handlerSchema.method) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide method', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in question delegate:', plugin, handlerSchema, initResult); + + // Execute the main method and its JS. The template returned will be used in the question component. + return this.executeMethodAndJS(plugin, handlerSchema.method).then((result) => { + // Create and register the handler. + const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), + questionType = plugin.component, + questionHandler = new CoreSitePluginsQuestionHandler(uniqueName, questionType); + + // Store in handlerSchema some data required by the component. + handlerSchema.methodTemplates = result.templates; + handlerSchema.methodJSResult = result.jsResult; + + if (result && result.jsResult) { + // Override default handler functions with the result of the method JS. + for (const property in questionHandler) { + if (property != 'constructor' && typeof questionHandler[property] == 'function' && + typeof result.jsResult[property] == 'function') { + questionHandler[property] = result.jsResult[property].bind(questionHandler); + } + } + } + + this.questionDelegate.registerHandler(questionHandler); + + return questionType; + }).catch((err) => { + this.logger.error('Error executing main method for question', handlerSchema.method, err); + }); + } + /** * Given a handler in an plugin, register it in the settings delegate. * @@ -712,7 +769,7 @@ export class CoreSitePluginsHelperProvider { return fieldType; }).catch((err) => { - this.logger.error('Error executing main method', handlerSchema.method, err); + this.logger.error('Error executing main method for user profile field', handlerSchema.method, err); }); } } From bf658df6b4204f2510b9c460bda6afdb92b6cee6 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 9 May 2018 10:56:16 +0200 Subject: [PATCH 4/9] MOBILE-2376 siteplugins: Support question behaviours --- .../qbehaviour/adaptive/providers/handler.ts | 5 +- .../adaptivenopenalty/providers/handler.ts | 5 +- .../deferredcbm/providers/handler.ts | 5 +- .../immediatecbm/providers/handler.ts | 5 +- .../immediatefeedback/providers/handler.ts | 5 +- .../informationitem/providers/handler.ts | 5 +- .../interactive/providers/handler.ts | 5 +- .../interactivecountback/providers/handler.ts | 5 +- .../components/compile-html/compile-html.ts | 8 +-- src/core/compile/providers/compile.ts | 6 ++ .../question/components/question/question.ts | 10 ++-- .../question/providers/behaviour-delegate.ts | 10 ++-- .../providers/default-behaviour-handler.ts | 5 +- .../classes/compile-init-component.ts | 59 +++++++++++++++++++ .../classes/question-behaviour-handler.ts | 44 ++++++++++++++ .../components/components.module.ts | 10 +++- .../question-behaviour.html | 1 + .../question-behaviour/question-behaviour.ts | 58 ++++++++++++++++++ .../components/question/question.html | 2 +- .../components/question/question.ts | 46 +++------------ .../user-profile-field.html | 2 +- .../user-profile-field/user-profile-field.ts | 46 +++------------ src/core/siteplugins/providers/helper.ts | 59 +++++++++++++++++++ 23 files changed, 292 insertions(+), 114 deletions(-) create mode 100644 src/core/siteplugins/classes/compile-init-component.ts create mode 100644 src/core/siteplugins/classes/question-behaviour-handler.ts create mode 100644 src/core/siteplugins/components/question-behaviour/question-behaviour.html create mode 100644 src/core/siteplugins/components/question-behaviour/question-behaviour.ts diff --git a/src/addon/qbehaviour/adaptive/providers/handler.ts b/src/addon/qbehaviour/adaptive/providers/handler.ts index 37a5e908a..90ad015b8 100644 --- a/src/addon/qbehaviour/adaptive/providers/handler.ts +++ b/src/addon/qbehaviour/adaptive/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; @@ -34,11 +34,12 @@ export class AddonQbehaviourAdaptiveHandler implements CoreQuestionBehaviourHand * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { // Just extract the button, it doesn't need any specific component. this.questionHelper.extractQbehaviourButtons(question); diff --git a/src/addon/qbehaviour/adaptivenopenalty/providers/handler.ts b/src/addon/qbehaviour/adaptivenopenalty/providers/handler.ts index 3f788eea5..a8ebebb0b 100644 --- a/src/addon/qbehaviour/adaptivenopenalty/providers/handler.ts +++ b/src/addon/qbehaviour/adaptivenopenalty/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; @@ -34,11 +34,12 @@ export class AddonQbehaviourAdaptiveNoPenaltyHandler implements CoreQuestionBeha * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { // Just extract the button, it doesn't need any specific component. this.questionHelper.extractQbehaviourButtons(question); diff --git a/src/addon/qbehaviour/deferredcbm/providers/handler.ts b/src/addon/qbehaviour/deferredcbm/providers/handler.ts index e15d47c4d..fb11cb208 100644 --- a/src/addon/qbehaviour/deferredcbm/providers/handler.ts +++ b/src/addon/qbehaviour/deferredcbm/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; import { CoreQuestionState } from '@core/question/providers/question'; @@ -55,11 +55,12 @@ export class AddonQbehaviourDeferredCBMHandler implements CoreQuestionBehaviourH * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { if (this.questionHelper.extractQbehaviourCBM(question)) { return [AddonQbehaviourDeferredCBMComponent]; } diff --git a/src/addon/qbehaviour/immediatecbm/providers/handler.ts b/src/addon/qbehaviour/immediatecbm/providers/handler.ts index 0f1493809..4e1aa9ce4 100644 --- a/src/addon/qbehaviour/immediatecbm/providers/handler.ts +++ b/src/addon/qbehaviour/immediatecbm/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; import { AddonQbehaviourDeferredCBMComponent } from '@addon/qbehaviour/deferredcbm/component/deferredcbm'; @@ -35,11 +35,12 @@ export class AddonQbehaviourImmediateCBMHandler implements CoreQuestionBehaviour * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { // Just extract the button, it doesn't need any specific component. this.questionHelper.extractQbehaviourButtons(question); if (this.questionHelper.extractQbehaviourCBM(question)) { diff --git a/src/addon/qbehaviour/immediatefeedback/providers/handler.ts b/src/addon/qbehaviour/immediatefeedback/providers/handler.ts index 304e71bdc..2103bba6c 100644 --- a/src/addon/qbehaviour/immediatefeedback/providers/handler.ts +++ b/src/addon/qbehaviour/immediatefeedback/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; @@ -34,11 +34,12 @@ export class AddonQbehaviourImmediateFeedbackHandler implements CoreQuestionBeha * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { // Just extract the button, it doesn't need any specific component. this.questionHelper.extractQbehaviourButtons(question); diff --git a/src/addon/qbehaviour/informationitem/providers/handler.ts b/src/addon/qbehaviour/informationitem/providers/handler.ts index ac2df43f2..21d7181ed 100644 --- a/src/addon/qbehaviour/informationitem/providers/handler.ts +++ b/src/addon/qbehaviour/informationitem/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionProvider, CoreQuestionState } from '@core/question/providers/question'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; @@ -52,11 +52,12 @@ export class AddonQbehaviourInformationItemHandler implements CoreQuestionBehavi * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { if (this.questionHelper.extractQbehaviourSeenInput(question)) { return [AddonQbehaviourInformationItemComponent]; } diff --git a/src/addon/qbehaviour/interactive/providers/handler.ts b/src/addon/qbehaviour/interactive/providers/handler.ts index af6d6024c..5504da1cc 100644 --- a/src/addon/qbehaviour/interactive/providers/handler.ts +++ b/src/addon/qbehaviour/interactive/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; @@ -34,11 +34,12 @@ export class AddonQbehaviourInteractiveHandler implements CoreQuestionBehaviourH * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { // Just extract the button, it doesn't need any specific component. this.questionHelper.extractQbehaviourButtons(question); diff --git a/src/addon/qbehaviour/interactivecountback/providers/handler.ts b/src/addon/qbehaviour/interactivecountback/providers/handler.ts index 612abb104..54646e427 100644 --- a/src/addon/qbehaviour/interactivecountback/providers/handler.ts +++ b/src/addon/qbehaviour/interactivecountback/providers/handler.ts @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from '@core/question/providers/behaviour-delegate'; import { CoreQuestionHelperProvider } from '@core/question/providers/helper'; @@ -34,11 +34,12 @@ export class AddonQbehaviourInteractiveCountbackHandler implements CoreQuestionB * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { // Just extract the button, it doesn't need any specific component. this.questionHelper.extractQbehaviourButtons(question); diff --git a/src/core/compile/components/compile-html/compile-html.ts b/src/core/compile/components/compile-html/compile-html.ts index 16ca3b26f..c530b08f2 100644 --- a/src/core/compile/components/compile-html/compile-html.ts +++ b/src/core/compile/components/compile-html/compile-html.ts @@ -14,11 +14,10 @@ import { Component, Input, OnInit, OnChanges, OnDestroy, ViewContainerRef, ViewChild, ComponentRef, SimpleChange, ChangeDetectorRef, - ElementRef, Optional + ElementRef, Optional, Output, EventEmitter } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreCompileProvider } from '../../providers/compile'; -import { BehaviorSubject } from 'rxjs'; /** * This component has a behaviour similar to $compile for AngularJS. Given an HTML code, it will compile it so all its @@ -43,18 +42,17 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { @Input() text: string; // The HTML text to display. @Input() javascript: string; // The Javascript to execute in the component. @Input() jsData: any; // Data to pass to the fake component. + @Output() created: EventEmitter = new EventEmitter(); // Will emit an event when the component is instantiated. // Get the container where to put the content. @ViewChild('dynamicComponent', { read: ViewContainerRef }) container: ViewContainerRef; protected componentRef: ComponentRef; protected element; - componentObservable: BehaviorSubject; // An observable to notify observers when the component is instantiated. constructor(protected compileProvider: CoreCompileProvider, protected cdr: ChangeDetectorRef, element: ElementRef, @Optional() protected navCtrl: NavController) { this.element = element.nativeElement; - this.componentObservable = new BehaviorSubject(null); } /** @@ -70,7 +68,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy { if (factory) { // Create the component. this.componentRef = this.container.createComponent(factory); - this.componentObservable.next(this.componentRef.instance); + this.created.emit(this.componentRef.instance); } }); } diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index bbd988366..70b3c7fb1 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -75,6 +75,9 @@ import { CoreCourseFormatSingleActivityComponent } from '@core/course/formats/si import { CoreSitePluginsModuleIndexComponent } from '@core/siteplugins/components/module-index/module-index'; import { CoreSitePluginsCourseOptionComponent } from '@core/siteplugins/components/course-option/course-option'; import { CoreSitePluginsCourseFormatComponent } from '@core/siteplugins/components/course-format/course-format'; +import { CoreSitePluginsQuestionComponent } from '@core/siteplugins/components/question/question'; +import { CoreSitePluginsQuestionBehaviourComponent } from '@core/siteplugins/components/question-behaviour/question-behaviour'; +import { CoreSitePluginsUserProfileFieldComponent } from '@core/siteplugins/components/user-profile-field/user-profile-field'; /** * Service to provide functionalities regarding compiling dynamic HTML and Javascript. @@ -203,6 +206,9 @@ export class CoreCompileProvider { instance['CoreSitePluginsModuleIndexComponent'] = CoreSitePluginsModuleIndexComponent; instance['CoreSitePluginsCourseOptionComponent'] = CoreSitePluginsCourseOptionComponent; instance['CoreSitePluginsCourseFormatComponent'] = CoreSitePluginsCourseFormatComponent; + instance['CoreSitePluginsQuestionComponent'] = CoreSitePluginsQuestionComponent; + instance['CoreSitePluginsQuestionBehaviourComponent'] = CoreSitePluginsQuestionBehaviourComponent; + instance['CoreSitePluginsUserProfileFieldComponent'] = CoreSitePluginsUserProfileFieldComponent; } /** diff --git a/src/core/question/components/question/question.ts b/src/core/question/components/question/question.ts index 57913acc9..f5cc7666c 100644 --- a/src/core/question/components/question/question.ts +++ b/src/core/question/components/question/question.ts @@ -123,11 +123,15 @@ export class CoreQuestionComponent implements OnInit { promise.then(() => { // Handle behaviour. - this.behaviourDelegate.handleQuestion(this.question.preferredBehaviour, this.question).then((comps) => { + this.behaviourDelegate.handleQuestion(this.injector, this.question.preferredBehaviour, this.question) + .then((comps) => { this.behaviourComponents = comps; + }).finally(() => { + this.question.html = this.domUtils.removeElementFromHtml(this.question.html, '.im-controls'); + this.loaded = true; }); + this.questionHelper.extractQbehaviourRedoButton(this.question); - this.question.html = this.domUtils.removeElementFromHtml(this.question.html, '.im-controls'); // Extract the validation error of the question. this.question.validationError = this.questionHelper.getValidationErrorFromHtml(this.question.html); @@ -138,8 +142,6 @@ export class CoreQuestionComponent implements OnInit { // Try to extract the feedback and comment for the question. this.questionHelper.extractQuestionFeedback(this.question); this.questionHelper.extractQuestionComment(this.question); - - this.loaded = true; }); } }).catch(() => { diff --git a/src/core/question/providers/behaviour-delegate.ts b/src/core/question/providers/behaviour-delegate.ts index 3ec8e6249..9333e87f9 100644 --- a/src/core/question/providers/behaviour-delegate.ts +++ b/src/core/question/providers/behaviour-delegate.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; @@ -48,11 +48,12 @@ export interface CoreQuestionBehaviourHandler extends CoreDelegateHandler { * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion?(question: any): any[] | Promise; + handleQuestion?(injector: Injector, question: any): any[] | Promise; } /** @@ -90,14 +91,15 @@ export class CoreQuestionBehaviourDelegate extends CoreDelegate { * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return a directive to render it. * + * @param {Injector} injector Injector. * @param {string} behaviour Default behaviour. * @param {any} question The question. * @return {Promise} Promise resolved with components to render some extra data in the question. */ - handleQuestion(behaviour: string, question: any): Promise { + handleQuestion(injector: Injector, behaviour: string, question: any): Promise { behaviour = this.questionDelegate.getBehaviourForQuestion(question, behaviour); - return Promise.resolve(this.executeFunctionOnEnabled(behaviour, 'handleQuestion', [question])); + return Promise.resolve(this.executeFunctionOnEnabled(behaviour, 'handleQuestion', [injector, question])); } /** diff --git a/src/core/question/providers/default-behaviour-handler.ts b/src/core/question/providers/default-behaviour-handler.ts index 1eb790924..c26be4549 100644 --- a/src/core/question/providers/default-behaviour-handler.ts +++ b/src/core/question/providers/default-behaviour-handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { CoreQuestionBehaviourHandler } from './behaviour-delegate'; import { CoreQuestionProvider, CoreQuestionState } from '@core/question/providers/question'; @@ -46,11 +46,12 @@ export class CoreQuestionBehaviourDefaultHandler implements CoreQuestionBehaviou * If the behaviour requires a submit button, it should add it to question.behaviourButtons. * If the behaviour requires to show some extra data, it should return the components to render it. * + * @param {Injector} injector Injector. * @param {any} question The question. * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question * (e.g. certainty options). Don't return anything if no extra data is required. */ - handleQuestion(question: any): any[] | Promise { + handleQuestion(injector: Injector, question: any): any[] | Promise { // Nothing to do. return; } diff --git a/src/core/siteplugins/classes/compile-init-component.ts b/src/core/siteplugins/classes/compile-init-component.ts new file mode 100644 index 000000000..9f9e7270f --- /dev/null +++ b/src/core/siteplugins/classes/compile-init-component.ts @@ -0,0 +1,59 @@ +// (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 { CoreSitePluginsProvider } from '../providers/siteplugins'; + +/** + * Base class for components that will display a component using core-compile-html and want to call a + * componentInit function returned by the handler JS. + */ +export class CoreSitePluginsCompileInitComponent { + content = ''; // Content. + jsData: any; // Data to pass to the component. + protected handlerSchema: any; // The handler data. + + constructor(protected sitePluginsProvider: CoreSitePluginsProvider) { } + + /** + * Function called when the component is created. + * + * @param {any} instance The component instance. + */ + componentCreated(instance: any): void { + // Check if the JS defined an init function. + if (instance && this.handlerSchema && this.handlerSchema.methodJSResult && + this.handlerSchema.methodJSResult.componentInit) { + this.handlerSchema.methodJSResult.componentInit.apply(instance); + } + } + + /** + * Get the handler data. + * + * @param {string} name The name of the handler. + */ + getHandlerData(name: string): void { + // Retrieve the handler data. + const handler = this.sitePluginsProvider.getSitePluginHandler(name); + + this.handlerSchema = handler && handler.handlerSchema; + + if (this.handlerSchema) { + // Load first template. + if (this.handlerSchema.methodTemplates && this.handlerSchema.methodTemplates.length) { + this.content = handler.handlerSchema.methodTemplates[0].html; + } + } + } +} diff --git a/src/core/siteplugins/classes/question-behaviour-handler.ts b/src/core/siteplugins/classes/question-behaviour-handler.ts new file mode 100644 index 000000000..bf4adc7be --- /dev/null +++ b/src/core/siteplugins/classes/question-behaviour-handler.ts @@ -0,0 +1,44 @@ +// (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 { Injector } from '@angular/core'; +import { CoreQuestionBehaviourDefaultHandler } from '@core/question/providers/default-behaviour-handler'; +import { CoreSitePluginsQuestionBehaviourComponent } from '../components/question-behaviour/question-behaviour'; +import { CoreQuestionProvider } from '@core/question/providers/question'; + +/** + * Handler to display a question behaviour site plugin. + */ +export class CoreSitePluginsQuestionBehaviourHandler extends CoreQuestionBehaviourDefaultHandler { + + constructor(questionProvider: CoreQuestionProvider, public name: string, public type: string, public hasTemplate: boolean) { + super(questionProvider); + } + + /** + * Handle a question behaviour. + * If the behaviour requires a submit button, it should add it to question.behaviourButtons. + * If the behaviour requires to show some extra data, it should return the components to render it. + * + * @param {Injector} injector Injector. + * @param {any} question The question. + * @return {any[]|Promise} Components (or promise resolved with components) to render some extra data in the question + * (e.g. certainty options). Don't return anything if no extra data is required. + */ + handleQuestion(injector: Injector, question: any): any[] | Promise { + if (this.hasTemplate) { + return [CoreSitePluginsQuestionBehaviourComponent]; + } + } +} diff --git a/src/core/siteplugins/components/components.module.ts b/src/core/siteplugins/components/components.module.ts index 4c809ca39..f6fe3250f 100644 --- a/src/core/siteplugins/components/components.module.ts +++ b/src/core/siteplugins/components/components.module.ts @@ -24,6 +24,7 @@ import { CoreSitePluginsCourseOptionComponent } from './course-option/course-opt import { CoreSitePluginsCourseFormatComponent } from './course-format/course-format'; import { CoreSitePluginsUserProfileFieldComponent } from './user-profile-field/user-profile-field'; import { CoreSitePluginsQuestionComponent } from './question/question'; +import { CoreSitePluginsQuestionBehaviourComponent } from './question-behaviour/question-behaviour'; @NgModule({ declarations: [ @@ -32,7 +33,8 @@ import { CoreSitePluginsQuestionComponent } from './question/question'; CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, - CoreSitePluginsQuestionComponent + CoreSitePluginsQuestionComponent, + CoreSitePluginsQuestionBehaviourComponent ], imports: [ CommonModule, @@ -49,14 +51,16 @@ import { CoreSitePluginsQuestionComponent } from './question/question'; CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, - CoreSitePluginsQuestionComponent + CoreSitePluginsQuestionComponent, + CoreSitePluginsQuestionBehaviourComponent ], entryComponents: [ CoreSitePluginsModuleIndexComponent, CoreSitePluginsCourseOptionComponent, CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, - CoreSitePluginsQuestionComponent + CoreSitePluginsQuestionComponent, + CoreSitePluginsQuestionBehaviourComponent ] }) export class CoreSitePluginsComponentsModule {} diff --git a/src/core/siteplugins/components/question-behaviour/question-behaviour.html b/src/core/siteplugins/components/question-behaviour/question-behaviour.html new file mode 100644 index 000000000..f58bcd913 --- /dev/null +++ b/src/core/siteplugins/components/question-behaviour/question-behaviour.html @@ -0,0 +1 @@ + diff --git a/src/core/siteplugins/components/question-behaviour/question-behaviour.ts b/src/core/siteplugins/components/question-behaviour/question-behaviour.ts new file mode 100644 index 000000000..1d4777cb7 --- /dev/null +++ b/src/core/siteplugins/components/question-behaviour/question-behaviour.ts @@ -0,0 +1,58 @@ +// (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 { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; +import { CoreSitePluginsCompileInitComponent } from '../../classes/compile-init-component'; + +/** + * Component that displays a question behaviour created using a site plugin. + */ +@Component({ + selector: 'core-site-plugins-question-behaviour', + templateUrl: 'question-behaviour.html', +}) +export class CoreSitePluginsQuestionBehaviourComponent extends CoreSitePluginsCompileInitComponent implements OnInit { + @Input() question: any; // The question where the behaviour will be rendered. + @Input() component: string; // The component the question belongs to. + @Input() componentId: number; // ID of the component the question belongs to. + @Input() attemptId: number; // Attempt ID. + @Input() offlineEnabled?: boolean | string; // Whether the question can be answered in offline. + @Output() buttonClicked: EventEmitter; // Should emit an event when a behaviour button is clicked. + @Output() onAbort: EventEmitter; // Should emit an event if the question should be aborted. + + constructor(sitePluginsProvider: CoreSitePluginsProvider) { + super(sitePluginsProvider); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Pass the input and output data to the component. + this.jsData = { + question: this.question, + component: this.component, + componentId: this.componentId, + attemptId: this.attemptId, + offlineEnabled: this.offlineEnabled, + buttonClicked: this.buttonClicked, + onAbort: this.onAbort + }; + + if (this.question) { + this.getHandlerData('qbehaviour_' + this.question.preferredBehaviour); + } + } +} diff --git a/src/core/siteplugins/components/question/question.html b/src/core/siteplugins/components/question/question.html index fec5e4726..f58bcd913 100644 --- a/src/core/siteplugins/components/question/question.html +++ b/src/core/siteplugins/components/question/question.html @@ -1 +1 @@ - + diff --git a/src/core/siteplugins/components/question/question.ts b/src/core/siteplugins/components/question/question.ts index f7e46cc52..ccb6fbaa1 100644 --- a/src/core/siteplugins/components/question/question.ts +++ b/src/core/siteplugins/components/question/question.ts @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Input, Output, EventEmitter, ViewChild, OnDestroy } from '@angular/core'; +import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { CoreSitePluginsProvider } from '../../providers/siteplugins'; -import { CoreCompileHtmlComponent } from '@core/compile/components/compile-html/compile-html'; -import { Subscription } from 'rxjs'; +import { CoreSitePluginsCompileInitComponent } from '../../classes/compile-init-component'; /** * Component that displays a question created using a site plugin. @@ -24,7 +23,7 @@ import { Subscription } from 'rxjs'; selector: 'core-site-plugins-question', templateUrl: 'question.html', }) -export class CoreSitePluginsQuestionComponent implements OnInit, OnDestroy { +export class CoreSitePluginsQuestionComponent extends CoreSitePluginsCompileInitComponent implements OnInit { @Input() question: any; // The question to render. @Input() component: string; // The component the question belongs to. @Input() componentId: number; // ID of the component the question belongs to. @@ -33,13 +32,9 @@ export class CoreSitePluginsQuestionComponent implements OnInit, OnDestroy { @Output() buttonClicked: EventEmitter; // Should emit an event when a behaviour button is clicked. @Output() onAbort: EventEmitter; // Should emit an event if the question should be aborted. - @ViewChild(CoreCompileHtmlComponent) compileComponent: CoreCompileHtmlComponent; - - content = ''; // Content. - jsData; - protected componentObserver: Subscription; - - constructor(protected sitePluginsProvider: CoreSitePluginsProvider) { } + constructor(sitePluginsProvider: CoreSitePluginsProvider) { + super(sitePluginsProvider); + } /** * Component being initialized. @@ -57,34 +52,7 @@ export class CoreSitePluginsQuestionComponent implements OnInit, OnDestroy { }; if (this.question) { - // Retrieve the handler data. - const handler = this.sitePluginsProvider.getSitePluginHandler('qtype_' + this.question.type), - handlerSchema = handler && handler.handlerSchema; - - if (handlerSchema) { - // Load first template. - if (handlerSchema.methodTemplates && handlerSchema.methodTemplates.length) { - this.content = handler.handlerSchema.methodTemplates[0].html; - } - - // Wait for the instance to be created. - if (this.compileComponent && this.compileComponent.componentObservable && - handlerSchema.methodJSResult && handlerSchema.methodJSResult.componentInit) { - this.componentObserver = this.compileComponent.componentObservable.subscribe((instance) => { - if (instance) { - // Instance created, call component init. - handlerSchema.methodJSResult.componentInit.apply(instance); - } - }); - } - } + this.getHandlerData('qtype_' + this.question.type); } } - - /** - * Component destroyed. - */ - ngOnDestroy(): void { - this.componentObserver && this.componentObserver.unsubscribe(); - } } diff --git a/src/core/siteplugins/components/user-profile-field/user-profile-field.html b/src/core/siteplugins/components/user-profile-field/user-profile-field.html index fec5e4726..f58bcd913 100644 --- a/src/core/siteplugins/components/user-profile-field/user-profile-field.html +++ b/src/core/siteplugins/components/user-profile-field/user-profile-field.html @@ -1 +1 @@ - + diff --git a/src/core/siteplugins/components/user-profile-field/user-profile-field.ts b/src/core/siteplugins/components/user-profile-field/user-profile-field.ts index 84030ba62..f5c2271d2 100644 --- a/src/core/siteplugins/components/user-profile-field/user-profile-field.ts +++ b/src/core/siteplugins/components/user-profile-field/user-profile-field.ts @@ -12,10 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Input, ViewChild, OnDestroy } from '@angular/core'; +import { Component, OnInit, Input } from '@angular/core'; import { CoreSitePluginsProvider } from '../../providers/siteplugins'; -import { CoreCompileHtmlComponent } from '@core/compile/components/compile-html/compile-html'; -import { Subscription } from 'rxjs'; +import { CoreSitePluginsCompileInitComponent } from '../../classes/compile-init-component'; /** * Component that displays a user profile field created using a site plugin. @@ -24,20 +23,16 @@ import { Subscription } from 'rxjs'; selector: 'core-site-plugins-user-profile-field', templateUrl: 'user-profile-field.html', }) -export class CoreSitePluginsUserProfileFieldComponent implements OnInit, OnDestroy { +export class CoreSitePluginsUserProfileFieldComponent extends CoreSitePluginsCompileInitComponent implements OnInit { @Input() field: any; // The profile field to be rendered. @Input() signup = false; // True if editing the field in signup. Defaults to false. @Input() edit = false; // True if editing the field. Defaults to false. @Input() form?: any; // Form where to add the form control. Required if edit=true or signup=true. @Input() registerAuth?: string; // Register auth method. E.g. 'email'. - @ViewChild(CoreCompileHtmlComponent) compileComponent: CoreCompileHtmlComponent; - - content = ''; // Content. - jsData; - protected componentObserver: Subscription; - - constructor(protected sitePluginsProvider: CoreSitePluginsProvider) { } + constructor(sitePluginsProvider: CoreSitePluginsProvider) { + super(sitePluginsProvider); + } /** * Component being initialized. @@ -54,34 +49,7 @@ export class CoreSitePluginsUserProfileFieldComponent implements OnInit, OnDestr }; if (this.field) { - // Retrieve the handler data. - const handler = this.sitePluginsProvider.getSitePluginHandler(this.field.type || this.field.datatype), - handlerSchema = handler && handler.handlerSchema; - - if (handlerSchema) { - // Load first template. - if (handlerSchema.methodTemplates && handlerSchema.methodTemplates.length) { - this.content = handler.handlerSchema.methodTemplates[0].html; - } - - // Wait for the instance to be created. - if (this.compileComponent && this.compileComponent.componentObservable && - handlerSchema.methodJSResult && handlerSchema.methodJSResult.componentInit) { - this.componentObserver = this.compileComponent.componentObservable.subscribe((instance) => { - if (instance) { - // Instance created, call component init. - handlerSchema.methodJSResult.componentInit.apply(instance); - } - }); - } - } + this.getHandlerData(this.field.type || this.field.datatype); } } - - /** - * Component destroyed. - */ - ngOnDestroy(): void { - this.componentObserver && this.componentObserver.unsubscribe(); - } } diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index cdd570dd9..65116a0e9 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -25,6 +25,7 @@ import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSitePluginsProvider } from './siteplugins'; import { CoreCompileProvider } from '@core/compile/providers/compile'; +import { CoreQuestionProvider } from '@core/question/providers/question'; // Delegates import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate'; @@ -36,6 +37,7 @@ import { CoreUserDelegate } from '@core/user/providers/user-delegate'; import { CoreUserProfileFieldDelegate } from '@core/user/providers/user-profile-field-delegate'; import { CoreSettingsDelegate } from '@core/settings/providers/delegate'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; +import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate'; import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/delegate'; // Handler classes. @@ -48,6 +50,7 @@ import { CoreSitePluginsUserProfileHandler } from '../classes/user-handler'; import { CoreSitePluginsUserProfileFieldHandler } from '../classes/user-profile-field-handler'; import { CoreSitePluginsSettingsHandler } from '../classes/settings-handler'; import { CoreSitePluginsQuestionHandler } from '../classes/question-handler'; +import { CoreSitePluginsQuestionBehaviourHandler } from '../classes/question-behaviour-handler'; import { CoreSitePluginsMessageOutputHandler } from '../classes/message-output-handler'; /** @@ -72,6 +75,7 @@ export class CoreSitePluginsHelperProvider { private courseFormatDelegate: CoreCourseFormatDelegate, private profileFieldDelegate: CoreUserProfileFieldDelegate, private textUtils: CoreTextUtilsProvider, private filepoolProvider: CoreFilepoolProvider, private settingsDelegate: CoreSettingsDelegate, private questionDelegate: CoreQuestionDelegate, + private questionBehaviourDelegate: CoreQuestionBehaviourDelegate, private questionProvider: CoreQuestionProvider, private messageOutputDelegate: AddonMessageOutputDelegate) { this.logger = logger.getInstance('CoreSitePluginsHelperProvider'); @@ -450,6 +454,10 @@ export class CoreSitePluginsHelperProvider { promise = Promise.resolve(this.registerQuestionHandler(plugin, handlerName, handlerSchema, result)); break; + case 'CoreQuestionBehaviourDelegate': + promise = Promise.resolve(this.registerQuestionBehaviourHandler(plugin, handlerName, handlerSchema, result)); + break; + case 'AddonMessageOutputDelegate': promise = Promise.resolve(this.registerMessageOutputHandler(plugin, handlerName, handlerSchema, result)); break; @@ -666,6 +674,57 @@ export class CoreSitePluginsHelperProvider { }); } + /** + * Given a handler in an plugin, register it in the question behaviour delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} initResult Result of the init WS call. + * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. + */ + protected registerQuestionBehaviourHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any) + : string | Promise { + if (!handlerSchema.method) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide method', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in question behaviour delegate:', plugin, handlerSchema, initResult); + + // Execute the main method and its JS. The template returned will be used in the question component. + return this.executeMethodAndJS(plugin, handlerSchema.method).then((result) => { + + // Create and register the handler. + const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), + type = plugin.component.replace('qbehaviour_', ''), + behaviourHandler = new CoreSitePluginsQuestionBehaviourHandler(this.questionProvider, uniqueName, type, + result.templates.length); + + // Store in handlerSchema some data required by the component. + handlerSchema.methodTemplates = result.templates; + handlerSchema.methodJSResult = result.jsResult; + + if (result && result.jsResult) { + // Override default handler functions with the result of the method JS. + for (const property in behaviourHandler) { + if (property != 'constructor' && typeof behaviourHandler[property] == 'function' && + typeof result.jsResult[property] == 'function') { + behaviourHandler[property] = result.jsResult[property].bind(behaviourHandler); + } + } + } + + this.questionBehaviourDelegate.registerHandler(behaviourHandler); + + return plugin.component; + }).catch((err) => { + this.logger.error('Error executing main method for question', handlerSchema.method, err); + }); + } + /** * Given a handler in an plugin, register it in the settings delegate. * From d7dd3acd8eb60696f955f24f463dbf9b5a89eea9 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 9 May 2018 12:33:06 +0200 Subject: [PATCH 5/9] MOBILE-2376 siteplugins: Support quiz access rules --- .../component/offlineattempts.ts | 1 + .../password/component/password.ts | 1 + .../timelimit/component/timelimit.ts | 1 + .../preflight-modal/preflight-modal.html | 4 +- .../pages/preflight-modal/preflight-modal.ts | 26 ++--- src/core/compile/providers/compile.ts | 2 + .../classes/{ => handlers}/base-handler.ts | 0 .../{ => handlers}/course-format-handler.ts | 2 +- .../{ => handlers}/course-option-handler.ts | 4 +- .../{ => handlers}/main-menu-handler.ts | 0 .../{ => handlers}/message-output-handler.ts | 0 .../classes/{ => handlers}/module-handler.ts | 2 +- .../{ => handlers}/module-prefetch-handler.ts | 2 +- .../question-behaviour-handler.ts | 2 +- .../{ => handlers}/question-handler.ts | 2 +- .../handlers/quiz-access-rule-handler.ts | 110 ++++++++++++++++++ .../{ => handlers}/settings-handler.ts | 0 .../classes/{ => handlers}/user-handler.ts | 2 +- .../user-profile-field-handler.ts | 2 +- .../components/components.module.ts | 10 +- .../quiz-access-rule/quiz-access-rule.html | 1 + .../quiz-access-rule/quiz-access-rule.ts | 57 +++++++++ src/core/siteplugins/providers/helper.ts | 100 ++++++++++++---- 23 files changed, 281 insertions(+), 50 deletions(-) rename src/core/siteplugins/classes/{ => handlers}/base-handler.ts (100%) rename src/core/siteplugins/classes/{ => handlers}/course-format-handler.ts (96%) rename src/core/siteplugins/classes/{ => handlers}/course-option-handler.ts (95%) rename src/core/siteplugins/classes/{ => handlers}/main-menu-handler.ts (100%) rename src/core/siteplugins/classes/{ => handlers}/message-output-handler.ts (100%) rename src/core/siteplugins/classes/{ => handlers}/module-handler.ts (97%) rename src/core/siteplugins/classes/{ => handlers}/module-prefetch-handler.ts (99%) rename src/core/siteplugins/classes/{ => handlers}/question-behaviour-handler.ts (94%) rename src/core/siteplugins/classes/{ => handlers}/question-handler.ts (94%) create mode 100644 src/core/siteplugins/classes/handlers/quiz-access-rule-handler.ts rename src/core/siteplugins/classes/{ => handlers}/settings-handler.ts (100%) rename src/core/siteplugins/classes/{ => handlers}/user-handler.ts (98%) rename src/core/siteplugins/classes/{ => handlers}/user-profile-field-handler.ts (95%) create mode 100644 src/core/siteplugins/components/quiz-access-rule/quiz-access-rule.html create mode 100644 src/core/siteplugins/components/quiz-access-rule/quiz-access-rule.ts diff --git a/src/addon/mod/quiz/accessrules/offlineattempts/component/offlineattempts.ts b/src/addon/mod/quiz/accessrules/offlineattempts/component/offlineattempts.ts index d23fd6927..f271fbff5 100644 --- a/src/addon/mod/quiz/accessrules/offlineattempts/component/offlineattempts.ts +++ b/src/addon/mod/quiz/accessrules/offlineattempts/component/offlineattempts.ts @@ -24,6 +24,7 @@ import { FormGroup, FormBuilder } from '@angular/forms'; }) export class AddonModQuizAccessOfflineAttemptsComponent implements OnInit { + @Input() rule: string; // The name of the rule. @Input() quiz: any; // The quiz the rule belongs to. @Input() attempt: any; // The attempt being started/continued. @Input() prefetch: boolean; // Whether the user is prefetching the quiz. diff --git a/src/addon/mod/quiz/accessrules/password/component/password.ts b/src/addon/mod/quiz/accessrules/password/component/password.ts index 6205901e4..00816c8f7 100644 --- a/src/addon/mod/quiz/accessrules/password/component/password.ts +++ b/src/addon/mod/quiz/accessrules/password/component/password.ts @@ -24,6 +24,7 @@ import { FormGroup, FormBuilder } from '@angular/forms'; }) export class AddonModQuizAccessPasswordComponent implements OnInit { + @Input() rule: string; // The name of the rule. @Input() quiz: any; // The quiz the rule belongs to. @Input() attempt: any; // The attempt being started/continued. @Input() prefetch: boolean; // Whether the user is prefetching the quiz. diff --git a/src/addon/mod/quiz/accessrules/timelimit/component/timelimit.ts b/src/addon/mod/quiz/accessrules/timelimit/component/timelimit.ts index 27d19d661..de23402d3 100644 --- a/src/addon/mod/quiz/accessrules/timelimit/component/timelimit.ts +++ b/src/addon/mod/quiz/accessrules/timelimit/component/timelimit.ts @@ -24,6 +24,7 @@ import { FormGroup } from '@angular/forms'; }) export class AddonModQuizAccessTimeLimitComponent { + @Input() rule: string; // The name of the rule. @Input() quiz: any; // The quiz the rule belongs to. @Input() attempt: any; // The attempt being started/continued. @Input() prefetch: boolean; // Whether the user is prefetching the quiz. diff --git a/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.html b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.html index db49a795e..716bb568f 100644 --- a/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.html +++ b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.html @@ -12,8 +12,8 @@
- - + +

Couldn't find the directive to render this access rule.

diff --git a/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.ts b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.ts index 48e704f54..6a9d3b124 100644 --- a/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.ts +++ b/src/addon/mod/quiz/pages/preflight-modal/preflight-modal.ts @@ -34,8 +34,7 @@ export class AddonModQuizPreflightModalPage implements OnInit { preflightForm: FormGroup; title: string; - accessRulesComponent: any[] = []; - data: any; + accessRulesData: {component: any, data: any}[] = []; // Components and data for each access rule. loaded: boolean; protected quiz: any; @@ -43,7 +42,6 @@ export class AddonModQuizPreflightModalPage implements OnInit { protected prefetch: boolean; protected siteId: string; protected rules: string[]; - protected renderedRules: string[] = []; constructor(params: NavParams, fb: FormBuilder, translate: TranslateService, sitesProvider: CoreSitesProvider, protected viewCtrl: ViewController, protected accessRuleDelegate: AddonModQuizAccessRuleDelegate, @@ -58,15 +56,6 @@ export class AddonModQuizPreflightModalPage implements OnInit { // Create an empty form group. The controls will be added by the access rules components. this.preflightForm = fb.group({}); - - // Create the data to pass to the access rules components. - this.data = { - quiz: this.quiz, - attempt: this.attempt, - prefetch: this.prefetch, - form: this.preflightForm, - siteId: this.siteId - }; } /** @@ -83,8 +72,17 @@ export class AddonModQuizPreflightModalPage implements OnInit { if (required) { return this.accessRuleDelegate.getPreflightComponent(rule, this.injector).then((component) => { if (component) { - this.renderedRules.push(rule); - this.accessRulesComponent.push(component); + this.accessRulesData.push({ + component: component, + data: { + rule: rule, + quiz: this.quiz, + attempt: this.attempt, + prefetch: this.prefetch, + form: this.preflightForm, + siteId: this.siteId + } + }); } }); } diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index 70b3c7fb1..68c67cc42 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -78,6 +78,7 @@ import { CoreSitePluginsCourseFormatComponent } from '@core/siteplugins/componen import { CoreSitePluginsQuestionComponent } from '@core/siteplugins/components/question/question'; import { CoreSitePluginsQuestionBehaviourComponent } from '@core/siteplugins/components/question-behaviour/question-behaviour'; import { CoreSitePluginsUserProfileFieldComponent } from '@core/siteplugins/components/user-profile-field/user-profile-field'; +import { CoreSitePluginsQuizAccessRuleComponent } from '@core/siteplugins/components/quiz-access-rule/quiz-access-rule'; /** * Service to provide functionalities regarding compiling dynamic HTML and Javascript. @@ -209,6 +210,7 @@ export class CoreCompileProvider { instance['CoreSitePluginsQuestionComponent'] = CoreSitePluginsQuestionComponent; instance['CoreSitePluginsQuestionBehaviourComponent'] = CoreSitePluginsQuestionBehaviourComponent; instance['CoreSitePluginsUserProfileFieldComponent'] = CoreSitePluginsUserProfileFieldComponent; + instance['CoreSitePluginsQuizAccessRuleComponent'] = CoreSitePluginsQuizAccessRuleComponent; } /** diff --git a/src/core/siteplugins/classes/base-handler.ts b/src/core/siteplugins/classes/handlers/base-handler.ts similarity index 100% rename from src/core/siteplugins/classes/base-handler.ts rename to src/core/siteplugins/classes/handlers/base-handler.ts diff --git a/src/core/siteplugins/classes/course-format-handler.ts b/src/core/siteplugins/classes/handlers/course-format-handler.ts similarity index 96% rename from src/core/siteplugins/classes/course-format-handler.ts rename to src/core/siteplugins/classes/handlers/course-format-handler.ts index ed31e6db4..b8453ca2a 100644 --- a/src/core/siteplugins/classes/course-format-handler.ts +++ b/src/core/siteplugins/classes/handlers/course-format-handler.ts @@ -15,7 +15,7 @@ import { Injector } from '@angular/core'; import { CoreCourseFormatHandler } from '@core/course/providers/format-delegate'; import { CoreSitePluginsBaseHandler } from './base-handler'; -import { CoreSitePluginsCourseFormatComponent } from '../components/course-format/course-format'; +import { CoreSitePluginsCourseFormatComponent } from '../../components/course-format/course-format'; /** * Handler to support a course format using a site plugin. diff --git a/src/core/siteplugins/classes/course-option-handler.ts b/src/core/siteplugins/classes/handlers/course-option-handler.ts similarity index 95% rename from src/core/siteplugins/classes/course-option-handler.ts rename to src/core/siteplugins/classes/handlers/course-option-handler.ts index 66b2c5c6f..0ecaa7515 100644 --- a/src/core/siteplugins/classes/course-option-handler.ts +++ b/src/core/siteplugins/classes/handlers/course-option-handler.ts @@ -13,10 +13,10 @@ // limitations under the License. import { Injector } from '@angular/core'; -import { CoreSitePluginsProvider } from '../providers/siteplugins'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate'; import { CoreSitePluginsBaseHandler } from './base-handler'; -import { CoreSitePluginsCourseOptionComponent } from '../components/course-option/course-option'; +import { CoreSitePluginsCourseOptionComponent } from '../../components/course-option/course-option'; /** * Handler to display a site plugin in course options. diff --git a/src/core/siteplugins/classes/main-menu-handler.ts b/src/core/siteplugins/classes/handlers/main-menu-handler.ts similarity index 100% rename from src/core/siteplugins/classes/main-menu-handler.ts rename to src/core/siteplugins/classes/handlers/main-menu-handler.ts diff --git a/src/core/siteplugins/classes/message-output-handler.ts b/src/core/siteplugins/classes/handlers/message-output-handler.ts similarity index 100% rename from src/core/siteplugins/classes/message-output-handler.ts rename to src/core/siteplugins/classes/handlers/message-output-handler.ts diff --git a/src/core/siteplugins/classes/module-handler.ts b/src/core/siteplugins/classes/handlers/module-handler.ts similarity index 97% rename from src/core/siteplugins/classes/module-handler.ts rename to src/core/siteplugins/classes/handlers/module-handler.ts index ee09ead5f..ffaf73af6 100644 --- a/src/core/siteplugins/classes/module-handler.ts +++ b/src/core/siteplugins/classes/handlers/module-handler.ts @@ -16,7 +16,7 @@ import { Injector } from '@angular/core'; import { NavController, NavOptions } from 'ionic-angular'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; import { CoreSitePluginsBaseHandler } from './base-handler'; -import { CoreSitePluginsModuleIndexComponent } from '../components/module-index/module-index'; +import { CoreSitePluginsModuleIndexComponent } from '../../components/module-index/module-index'; /** * Handler to support a module using a site plugin. diff --git a/src/core/siteplugins/classes/module-prefetch-handler.ts b/src/core/siteplugins/classes/handlers/module-prefetch-handler.ts similarity index 99% rename from src/core/siteplugins/classes/module-prefetch-handler.ts rename to src/core/siteplugins/classes/handlers/module-prefetch-handler.ts index a6d6d836f..df289cc97 100644 --- a/src/core/siteplugins/classes/module-prefetch-handler.ts +++ b/src/core/siteplugins/classes/handlers/module-prefetch-handler.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injector } from '@angular/core'; -import { CoreSitePluginsProvider } from '../providers/siteplugins'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler'; /** diff --git a/src/core/siteplugins/classes/question-behaviour-handler.ts b/src/core/siteplugins/classes/handlers/question-behaviour-handler.ts similarity index 94% rename from src/core/siteplugins/classes/question-behaviour-handler.ts rename to src/core/siteplugins/classes/handlers/question-behaviour-handler.ts index bf4adc7be..5352f6a67 100644 --- a/src/core/siteplugins/classes/question-behaviour-handler.ts +++ b/src/core/siteplugins/classes/handlers/question-behaviour-handler.ts @@ -14,7 +14,7 @@ import { Injector } from '@angular/core'; import { CoreQuestionBehaviourDefaultHandler } from '@core/question/providers/default-behaviour-handler'; -import { CoreSitePluginsQuestionBehaviourComponent } from '../components/question-behaviour/question-behaviour'; +import { CoreSitePluginsQuestionBehaviourComponent } from '../../components/question-behaviour/question-behaviour'; import { CoreQuestionProvider } from '@core/question/providers/question'; /** diff --git a/src/core/siteplugins/classes/question-handler.ts b/src/core/siteplugins/classes/handlers/question-handler.ts similarity index 94% rename from src/core/siteplugins/classes/question-handler.ts rename to src/core/siteplugins/classes/handlers/question-handler.ts index 4950468ca..7e9b128c7 100644 --- a/src/core/siteplugins/classes/question-handler.ts +++ b/src/core/siteplugins/classes/handlers/question-handler.ts @@ -14,7 +14,7 @@ import { Injector } from '@angular/core'; import { CoreQuestionDefaultHandler } from '@core/question/providers/default-question-handler'; -import { CoreSitePluginsQuestionComponent } from '../components/question/question'; +import { CoreSitePluginsQuestionComponent } from '../../components/question/question'; /** * Handler to display a question site plugin. diff --git a/src/core/siteplugins/classes/handlers/quiz-access-rule-handler.ts b/src/core/siteplugins/classes/handlers/quiz-access-rule-handler.ts new file mode 100644 index 000000000..e877c9a28 --- /dev/null +++ b/src/core/siteplugins/classes/handlers/quiz-access-rule-handler.ts @@ -0,0 +1,110 @@ +// (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 { Injector } from '@angular/core'; +import { CoreQuestionDefaultHandler } from '@core/question/providers/default-question-handler'; +import { CoreSitePluginsQuizAccessRuleComponent } from '../../components/quiz-access-rule/quiz-access-rule'; + +/** + * Handler to display a quiz access rule site plugin. + */ +export class CoreSitePluginsQuizAccessRuleHandler extends CoreQuestionDefaultHandler { + + constructor(public name: string, public ruleName: string, public hasTemplate: boolean) { + super(); + } + + /** + * 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. If not supplied, user is starting a new attempt. + * @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 { + return this.hasTemplate; + } + + /** + * 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} preflightData Object where to add the preflight data. + * @param {any} [attempt] The attempt started/continued. If not supplied, user is starting a new attempt. + * @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, preflightData: any, attempt?: any, prefetch?: boolean, siteId?: string): void | Promise { + // Nothing to do. + } + + /** + * 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 { + if (this.hasTemplate) { + return CoreSitePluginsQuizAccessRuleComponent; + } + } + + /** + * 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 { + // Nothing to do. + } + + /** + * 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 { + // Nothing to do. + } + + /** + * 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 { + return false; + } +} diff --git a/src/core/siteplugins/classes/settings-handler.ts b/src/core/siteplugins/classes/handlers/settings-handler.ts similarity index 100% rename from src/core/siteplugins/classes/settings-handler.ts rename to src/core/siteplugins/classes/handlers/settings-handler.ts diff --git a/src/core/siteplugins/classes/user-handler.ts b/src/core/siteplugins/classes/handlers/user-handler.ts similarity index 98% rename from src/core/siteplugins/classes/user-handler.ts rename to src/core/siteplugins/classes/handlers/user-handler.ts index fb4851f61..044fd2317 100644 --- a/src/core/siteplugins/classes/user-handler.ts +++ b/src/core/siteplugins/classes/handlers/user-handler.ts @@ -14,7 +14,7 @@ import { NavController } from 'ionic-angular'; import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate'; -import { CoreSitePluginsProvider } from '../providers/siteplugins'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; import { CoreSitePluginsBaseHandler } from './base-handler'; /** diff --git a/src/core/siteplugins/classes/user-profile-field-handler.ts b/src/core/siteplugins/classes/handlers/user-profile-field-handler.ts similarity index 95% rename from src/core/siteplugins/classes/user-profile-field-handler.ts rename to src/core/siteplugins/classes/handlers/user-profile-field-handler.ts index 36ae73268..bd7c1250a 100644 --- a/src/core/siteplugins/classes/user-profile-field-handler.ts +++ b/src/core/siteplugins/classes/handlers/user-profile-field-handler.ts @@ -15,7 +15,7 @@ import { Injector } from '@angular/core'; import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@core/user/providers/user-profile-field-delegate'; import { CoreSitePluginsBaseHandler } from './base-handler'; -import { CoreSitePluginsUserProfileFieldComponent } from '../components/user-profile-field/user-profile-field'; +import { CoreSitePluginsUserProfileFieldComponent } from '../../components/user-profile-field/user-profile-field'; /** * Handler to display a site plugin in the user profile. diff --git a/src/core/siteplugins/components/components.module.ts b/src/core/siteplugins/components/components.module.ts index f6fe3250f..c83d80db8 100644 --- a/src/core/siteplugins/components/components.module.ts +++ b/src/core/siteplugins/components/components.module.ts @@ -25,6 +25,7 @@ import { CoreSitePluginsCourseFormatComponent } from './course-format/course-for import { CoreSitePluginsUserProfileFieldComponent } from './user-profile-field/user-profile-field'; import { CoreSitePluginsQuestionComponent } from './question/question'; import { CoreSitePluginsQuestionBehaviourComponent } from './question-behaviour/question-behaviour'; +import { CoreSitePluginsQuizAccessRuleComponent } from './quiz-access-rule/quiz-access-rule'; @NgModule({ declarations: [ @@ -34,7 +35,8 @@ import { CoreSitePluginsQuestionBehaviourComponent } from './question-behaviour/ CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, CoreSitePluginsQuestionComponent, - CoreSitePluginsQuestionBehaviourComponent + CoreSitePluginsQuestionBehaviourComponent, + CoreSitePluginsQuizAccessRuleComponent ], imports: [ CommonModule, @@ -52,7 +54,8 @@ import { CoreSitePluginsQuestionBehaviourComponent } from './question-behaviour/ CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, CoreSitePluginsQuestionComponent, - CoreSitePluginsQuestionBehaviourComponent + CoreSitePluginsQuestionBehaviourComponent, + CoreSitePluginsQuizAccessRuleComponent ], entryComponents: [ CoreSitePluginsModuleIndexComponent, @@ -60,7 +63,8 @@ import { CoreSitePluginsQuestionBehaviourComponent } from './question-behaviour/ CoreSitePluginsCourseFormatComponent, CoreSitePluginsUserProfileFieldComponent, CoreSitePluginsQuestionComponent, - CoreSitePluginsQuestionBehaviourComponent + CoreSitePluginsQuestionBehaviourComponent, + CoreSitePluginsQuizAccessRuleComponent ] }) export class CoreSitePluginsComponentsModule {} diff --git a/src/core/siteplugins/components/quiz-access-rule/quiz-access-rule.html b/src/core/siteplugins/components/quiz-access-rule/quiz-access-rule.html new file mode 100644 index 000000000..f58bcd913 --- /dev/null +++ b/src/core/siteplugins/components/quiz-access-rule/quiz-access-rule.html @@ -0,0 +1 @@ + diff --git a/src/core/siteplugins/components/quiz-access-rule/quiz-access-rule.ts b/src/core/siteplugins/components/quiz-access-rule/quiz-access-rule.ts new file mode 100644 index 000000000..cfd9d1394 --- /dev/null +++ b/src/core/siteplugins/components/quiz-access-rule/quiz-access-rule.ts @@ -0,0 +1,57 @@ +// (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 { Component, OnInit, Input } from '@angular/core'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; +import { CoreSitePluginsCompileInitComponent } from '../../classes/compile-init-component'; +import { FormGroup } from '@angular/forms'; + +/** + * Component that displays a quiz access rule created using a site plugin. + */ +@Component({ + selector: 'core-site-plugins-quiz-access-rule', + templateUrl: 'quiz-access-rule.html', +}) +export class CoreSitePluginsQuizAccessRuleComponent extends CoreSitePluginsCompileInitComponent implements OnInit { + @Input() rule: string; // The name of the rule. + @Input() quiz: any; // The quiz the rule belongs to. + @Input() attempt: any; // The attempt being started/continued. + @Input() prefetch: boolean; // Whether the user is prefetching the quiz. + @Input() siteId: string; // Site ID. + @Input() form: FormGroup; // Form where to add the form control. + + constructor(sitePluginsProvider: CoreSitePluginsProvider) { + super(sitePluginsProvider); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Pass the input and output data to the component. + this.jsData = { + rule: this.rule, + quiz: this.quiz, + attempt: this.attempt, + prefetch: this.prefetch, + siteId: this.siteId, + form: this.form + }; + + if (this.rule) { + this.getHandlerData(this.rule); + } + } +} diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index 65116a0e9..5a05e7363 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -39,19 +39,21 @@ import { CoreSettingsDelegate } from '@core/settings/providers/delegate'; import { CoreQuestionDelegate } from '@core/question/providers/delegate'; import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate'; import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/delegate'; +import { AddonModQuizAccessRuleDelegate } from '@addon/mod/quiz/providers/access-rules-delegate'; // Handler classes. -import { CoreSitePluginsCourseFormatHandler } from '../classes/course-format-handler'; -import { CoreSitePluginsCourseOptionHandler } from '../classes/course-option-handler'; -import { CoreSitePluginsModuleHandler } from '../classes/module-handler'; -import { CoreSitePluginsModulePrefetchHandler } from '../classes/module-prefetch-handler'; -import { CoreSitePluginsMainMenuHandler } from '../classes/main-menu-handler'; -import { CoreSitePluginsUserProfileHandler } from '../classes/user-handler'; -import { CoreSitePluginsUserProfileFieldHandler } from '../classes/user-profile-field-handler'; -import { CoreSitePluginsSettingsHandler } from '../classes/settings-handler'; -import { CoreSitePluginsQuestionHandler } from '../classes/question-handler'; -import { CoreSitePluginsQuestionBehaviourHandler } from '../classes/question-behaviour-handler'; -import { CoreSitePluginsMessageOutputHandler } from '../classes/message-output-handler'; +import { CoreSitePluginsCourseFormatHandler } from '../classes/handlers/course-format-handler'; +import { CoreSitePluginsCourseOptionHandler } from '../classes/handlers/course-option-handler'; +import { CoreSitePluginsModuleHandler } from '../classes/handlers/module-handler'; +import { CoreSitePluginsModulePrefetchHandler } from '../classes/handlers/module-prefetch-handler'; +import { CoreSitePluginsMainMenuHandler } from '../classes/handlers/main-menu-handler'; +import { CoreSitePluginsUserProfileHandler } from '../classes/handlers/user-handler'; +import { CoreSitePluginsUserProfileFieldHandler } from '../classes/handlers/user-profile-field-handler'; +import { CoreSitePluginsSettingsHandler } from '../classes/handlers/settings-handler'; +import { CoreSitePluginsQuestionHandler } from '../classes/handlers/question-handler'; +import { CoreSitePluginsQuestionBehaviourHandler } from '../classes/handlers/question-behaviour-handler'; +import { CoreSitePluginsMessageOutputHandler } from '../classes/handlers/message-output-handler'; +import { CoreSitePluginsQuizAccessRuleHandler } from '../classes/handlers/quiz-access-rule-handler'; /** * Helper service to provide functionalities regarding site plugins. It basically has the features to load and register site @@ -76,7 +78,8 @@ export class CoreSitePluginsHelperProvider { private textUtils: CoreTextUtilsProvider, private filepoolProvider: CoreFilepoolProvider, private settingsDelegate: CoreSettingsDelegate, private questionDelegate: CoreQuestionDelegate, private questionBehaviourDelegate: CoreQuestionBehaviourDelegate, private questionProvider: CoreQuestionProvider, - private messageOutputDelegate: AddonMessageOutputDelegate) { + private messageOutputDelegate: AddonMessageOutputDelegate, + private accessRulesDelegate: AddonModQuizAccessRuleDelegate) { this.logger = logger.getInstance('CoreSitePluginsHelperProvider'); @@ -462,6 +465,10 @@ export class CoreSitePluginsHelperProvider { promise = Promise.resolve(this.registerMessageOutputHandler(plugin, handlerName, handlerSchema, result)); break; + case 'AddonModQuizAccessRuleDelegate': + promise = Promise.resolve(this.registerQuizAccessRuleHandler(plugin, handlerName, handlerSchema, result)); + break; + default: // Nothing to do. promise = Promise.resolve(); @@ -504,7 +511,7 @@ export class CoreSitePluginsHelperProvider { } /** - * Given a handler in an plugin, register it in the course options delegate. + * Given a handler in a plugin, register it in the course options delegate. * * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. @@ -533,7 +540,7 @@ export class CoreSitePluginsHelperProvider { } /** - * Given a handler in an plugin, register it in the main menu delegate. + * Given a handler in a plugin, register it in the main menu delegate. * * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. @@ -562,7 +569,7 @@ export class CoreSitePluginsHelperProvider { } /** - * Given a handler in an plugin, register it in the message output delegate. + * Given a handler in a plugin, register it in the message output delegate. * * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. @@ -592,7 +599,7 @@ export class CoreSitePluginsHelperProvider { } /** - * Given a handler in an plugin, register it in the module delegate. + * Given a handler in a plugin, register it in the module delegate. * * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. @@ -626,7 +633,7 @@ export class CoreSitePluginsHelperProvider { } /** - * Given a handler in an plugin, register it in the question delegate. + * Given a handler in a plugin, register it in the question delegate. * * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. @@ -675,7 +682,7 @@ export class CoreSitePluginsHelperProvider { } /** - * Given a handler in an plugin, register it in the question behaviour delegate. + * Given a handler in a plugin, register it in the question behaviour delegate. * * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. @@ -721,12 +728,61 @@ export class CoreSitePluginsHelperProvider { return plugin.component; }).catch((err) => { - this.logger.error('Error executing main method for question', handlerSchema.method, err); + this.logger.error('Error executing main method for question behaviour', handlerSchema.method, err); }); } /** - * Given a handler in an plugin, register it in the settings delegate. + * Given a handler in a plugin, register it in the quiz access rule delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} initResult Result of the init WS call. + * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. + */ + protected registerQuizAccessRuleHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any) + : string | Promise { + if (!handlerSchema.method) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide method', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in quiz access rule delegate:', plugin, handlerSchema, initResult); + + // Execute the main method and its JS. The template returned will be used in the access rule component. + return this.executeMethodAndJS(plugin, handlerSchema.method).then((result) => { + // Create and register the handler. + const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), + ruleName = plugin.component, + ruleHandler = new CoreSitePluginsQuizAccessRuleHandler(uniqueName, ruleName, result.templates.length); + + // Store in handlerSchema some data required by the component. + handlerSchema.methodTemplates = result.templates; + handlerSchema.methodJSResult = result.jsResult; + + if (result && result.jsResult) { + // Override default handler functions with the result of the method JS. + for (const property in ruleHandler) { + if (property != 'constructor' && typeof ruleHandler[property] == 'function' && + typeof result.jsResult[property] == 'function') { + ruleHandler[property] = result.jsResult[property].bind(ruleHandler); + } + } + } + + this.accessRulesDelegate.registerHandler(ruleHandler); + + return ruleName; + }).catch((err) => { + this.logger.error('Error executing main method for quiz access rule', handlerSchema.method, err); + }); + } + + /** + * Given a handler in a plugin, register it in the settings delegate. * * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. @@ -755,7 +811,7 @@ export class CoreSitePluginsHelperProvider { } /** - * Given a handler in an plugin, register it in the user profile delegate. + * Given a handler in a plugin, register it in the user profile delegate. * * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. @@ -784,7 +840,7 @@ export class CoreSitePluginsHelperProvider { } /** - * Given a handler in an plugin, register it in the user profile field delegate. + * Given a handler in a plugin, register it in the user profile field delegate. * * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. From 21647e7f241f3b8f73643d75eb9ff7a7139b0053 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 9 May 2018 15:16:17 +0200 Subject: [PATCH 6/9] MOBILE-2376 siteplugins: Support assign handlers --- .../providers/default-feedback-handler.ts | 82 +++++++++++- .../providers/default-submission-handler.ts | 107 +++++++++++++++- src/core/compile/providers/compile.ts | 4 + .../handlers/assign-feedback-handler.ts | 63 +++++++++ .../handlers/assign-submission-handler.ts | 72 +++++++++++ .../assign-feedback/assign-feedback.html | 1 + .../assign-feedback/assign-feedback.ts | 67 ++++++++++ .../assign-submission/assign-submission.html | 1 + .../assign-submission/assign-submission.ts | 65 ++++++++++ .../components/components.module.ts | 14 +- src/core/siteplugins/providers/helper.ts | 120 +++++++++++++++++- 11 files changed, 587 insertions(+), 9 deletions(-) create mode 100644 src/core/siteplugins/classes/handlers/assign-feedback-handler.ts create mode 100644 src/core/siteplugins/classes/handlers/assign-submission-handler.ts create mode 100644 src/core/siteplugins/components/assign-feedback/assign-feedback.html create mode 100644 src/core/siteplugins/components/assign-feedback/assign-feedback.ts create mode 100644 src/core/siteplugins/components/assign-submission/assign-submission.html create mode 100644 src/core/siteplugins/components/assign-submission/assign-submission.ts diff --git a/src/addon/mod/assign/providers/default-feedback-handler.ts b/src/addon/mod/assign/providers/default-feedback-handler.ts index 05466f140..0c3d03852 100644 --- a/src/addon/mod/assign/providers/default-feedback-handler.ts +++ b/src/addon/mod/assign/providers/default-feedback-handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { AddonModAssignFeedbackHandler } from './feedback-delegate'; @@ -24,7 +24,43 @@ export class AddonModAssignDefaultFeedbackHandler implements AddonModAssignFeedb name = 'AddonModAssignDefaultFeedbackHandler'; type = 'default'; - constructor(private translate: TranslateService) { } + constructor(protected translate: TranslateService) { } + + /** + * Discard the draft data of the feedback plugin. + * + * @param {number} assignId The assignment ID. + * @param {number} userId User ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} If the function is async, it should return a Promise resolved when done. + */ + discardDraft(assignId: number, userId: number, siteId?: string): void | Promise { + // Nothing to do. + } + + /** + * Return the Component to use to display the plugin data. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} plugin The plugin object. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any): any | Promise { + // Nothing to do. + } + + /** + * Return the draft saved data of the feedback plugin. + * + * @param {number} assignId The assignment ID. + * @param {number} userId User ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {any|Promise} Data (or promise resolved with the data). + */ + getDraft(assignId: number, userId: number, siteId?: string): any | Promise { + // Nothing to do. + } /** * Get files used by this plugin. @@ -95,4 +131,46 @@ export class AddonModAssignDefaultFeedbackHandler implements AddonModAssignFeedb isEnabled(): boolean | Promise { return true; } + + /** + * Prefetch any required data for the plugin. + * This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + prefetch(assign: any, submission: any, plugin: any, siteId?: string): Promise { + return Promise.resolve(); + } + + /** + * Prepare and add to pluginData the data to send to the server based on the draft data saved. + * + * @param {number} assignId The assignment ID. + * @param {number} userId User ID. + * @param {any} plugin The plugin object. + * @param {any} pluginData Object where to store the data to send. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} If the function is async, it should return a Promise resolved when done. + */ + prepareFeedbackData(assignId: number, userId: number, plugin: any, pluginData: any, siteId?: string): void | Promise { + // Nothing to do. + } + + /** + * Save draft data of the feedback plugin. + * + * @param {number} assignId The assignment ID. + * @param {number} userId User ID. + * @param {any} plugin The plugin object. + * @param {any} data The data to save. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} If the function is async, it should return a Promise resolved when done. + */ + saveDraft(assignId: number, userId: number, plugin: any, data: any, siteId?: string): void | Promise { + // Nothing to do. + } } diff --git a/src/addon/mod/assign/providers/default-submission-handler.ts b/src/addon/mod/assign/providers/default-submission-handler.ts index e473ad898..aa1f84056 100644 --- a/src/addon/mod/assign/providers/default-submission-handler.ts +++ b/src/addon/mod/assign/providers/default-submission-handler.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { AddonModAssignSubmissionHandler } from './submission-delegate'; @@ -24,7 +24,7 @@ export class AddonModAssignDefaultSubmissionHandler implements AddonModAssignSub name = 'AddonModAssignDefaultSubmissionHandler'; type = 'default'; - constructor(private translate: TranslateService) { } + constructor(protected translate: TranslateService) { } /** * Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the @@ -40,6 +40,60 @@ export class AddonModAssignDefaultSubmissionHandler implements AddonModAssignSub return false; } + /** + * Should clear temporary data for a cancelled submission. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {any} inputData Data entered by the user for the submission. + */ + clearTmpData(assign: any, submission: any, plugin: any, inputData: any): void { + // Nothing to do. + } + + /** + * This function will be called when the user wants to create a new submission based on the previous one. + * It should add to pluginData the data to send to server based in the data in plugin (previous attempt). + * + * @param {any} assign The assignment. + * @param {any} plugin The plugin object. + * @param {any} pluginData Object where to store the data to send. + * @param {number} [userId] User ID. If not defined, site's current user. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} If the function is async, it should return a Promise resolved when done. + */ + copySubmissionData(assign: any, plugin: any, pluginData: any, userId?: number, siteId?: string): void | Promise { + // Nothing to do. + } + + /** + * Delete any stored data for the plugin and submission. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {any} offlineData Offline data stored. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} If the function is async, it should return a Promise resolved when done. + */ + deleteOfflineData(assign: any, submission: any, plugin: any, offlineData: any, siteId?: string): void | Promise { + // Nothing to do. + } + + /** + * Return the Component to use to display the plugin data, either in read or in edit mode. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} plugin The plugin object. + * @param {boolean} [edit] Whether the user is editing. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise { + // Nothing to do. + } + /** * Get files used by this plugin. * The files returned by this function will be prefetched when the user prefetches the assign. @@ -127,4 +181,53 @@ export class AddonModAssignDefaultSubmissionHandler implements AddonModAssignSub isEnabledForEdit(): boolean | Promise { return false; } + + /** + * Prefetch any required data for the plugin. + * This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when done. + */ + prefetch(assign: any, submission: any, plugin: any, siteId?: string): Promise { + return Promise.resolve(); + } + + /** + * Prepare and add to pluginData the data to send to the server based on the input data. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {any} inputData Data entered by the user for the submission. + * @param {any} pluginData Object where to store the data to send. + * @param {boolean} [offline] Whether the user is editing in offline. + * @param {number} [userId] User ID. If not defined, site's current user. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} If the function is async, it should return a Promise resolved when done. + */ + prepareSubmissionData?(assign: any, submission: any, plugin: any, inputData: any, pluginData: any, offline?: boolean, + userId?: number, siteId?: string): void | Promise { + // Nothing to do. + } + + /** + * Prepare and add to pluginData the data to send to the server based on the offline data stored. + * This will be used when performing a synchronization. + * + * @param {any} assign The assignment. + * @param {any} submission The submission. + * @param {any} plugin The plugin object. + * @param {any} offlineData Offline data stored. + * @param {any} pluginData Object where to store the data to send. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {void|Promise} If the function is async, it should return a Promise resolved when done. + */ + prepareSyncData?(assign: any, submission: any, plugin: any, offlineData: any, pluginData: any, siteId?: string) + : void | Promise { + // Nothing to do. + } } diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index 68c67cc42..828238d0f 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -79,6 +79,8 @@ import { CoreSitePluginsQuestionComponent } from '@core/siteplugins/components/q import { CoreSitePluginsQuestionBehaviourComponent } from '@core/siteplugins/components/question-behaviour/question-behaviour'; import { CoreSitePluginsUserProfileFieldComponent } from '@core/siteplugins/components/user-profile-field/user-profile-field'; import { CoreSitePluginsQuizAccessRuleComponent } from '@core/siteplugins/components/quiz-access-rule/quiz-access-rule'; +import { CoreSitePluginsAssignFeedbackComponent } from '@core/siteplugins/components/assign-feedback/assign-feedback'; +import { CoreSitePluginsAssignSubmissionComponent } from '@core/siteplugins/components/assign-submission/assign-submission'; /** * Service to provide functionalities regarding compiling dynamic HTML and Javascript. @@ -211,6 +213,8 @@ export class CoreCompileProvider { instance['CoreSitePluginsQuestionBehaviourComponent'] = CoreSitePluginsQuestionBehaviourComponent; instance['CoreSitePluginsUserProfileFieldComponent'] = CoreSitePluginsUserProfileFieldComponent; instance['CoreSitePluginsQuizAccessRuleComponent'] = CoreSitePluginsQuizAccessRuleComponent; + instance['CoreSitePluginsAssignFeedbackComponent'] = CoreSitePluginsAssignFeedbackComponent; + instance['CoreSitePluginsAssignSubmissionComponent'] = CoreSitePluginsAssignSubmissionComponent; } /** diff --git a/src/core/siteplugins/classes/handlers/assign-feedback-handler.ts b/src/core/siteplugins/classes/handlers/assign-feedback-handler.ts new file mode 100644 index 000000000..04b64049f --- /dev/null +++ b/src/core/siteplugins/classes/handlers/assign-feedback-handler.ts @@ -0,0 +1,63 @@ +// (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 { Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonModAssignDefaultFeedbackHandler } from '@addon/mod/assign/providers/default-feedback-handler'; +import { CoreSitePluginsAssignFeedbackComponent } from '../../components/assign-feedback/assign-feedback'; + +/** + * Handler to display an assign feedback site plugin. + */ +export class CoreSitePluginsAssignFeedbackHandler extends AddonModAssignDefaultFeedbackHandler { + + constructor(translate: TranslateService, public name: string, public type: string, protected prefix: string) { + super(translate); + } + + /** + * Return the Component to use to display the plugin data, either in read or in edit mode. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} plugin The plugin object. + * @param {boolean} [edit] Whether the user is editing. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise { + return CoreSitePluginsAssignFeedbackComponent; + } + + /** + * Get a readable name to use for the plugin. + * + * @param {any} plugin The plugin object. + * @return {string} The plugin name. + */ + getPluginName(plugin: any): string { + // Check if there's a translated string for the plugin. + const translationId = this.prefix + 'pluginname', + translation = this.translate.instant(translationId); + + if (translationId != translation) { + // Translation found, use it. + return translation; + } + + // Fallback to WS string. + if (plugin.name) { + return plugin.name; + } + } +} diff --git a/src/core/siteplugins/classes/handlers/assign-submission-handler.ts b/src/core/siteplugins/classes/handlers/assign-submission-handler.ts new file mode 100644 index 000000000..c02a3cd7d --- /dev/null +++ b/src/core/siteplugins/classes/handlers/assign-submission-handler.ts @@ -0,0 +1,72 @@ +// (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 { Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { AddonModAssignDefaultSubmissionHandler } from '@addon/mod/assign/providers/default-submission-handler'; +import { CoreSitePluginsAssignSubmissionComponent } from '../../components/assign-submission/assign-submission'; + +/** + * Handler to display an assign submission site plugin. + */ +export class CoreSitePluginsAssignSubmissionHandler extends AddonModAssignDefaultSubmissionHandler { + + constructor(translate: TranslateService, public name: string, public type: string, protected prefix: string) { + super(translate); + } + + /** + * Return the Component to use to display the plugin data, either in read or in edit mode. + * It's recommended to return the class of the component, but you can also return an instance of the component. + * + * @param {Injector} injector Injector. + * @param {any} plugin The plugin object. + * @param {boolean} [edit] Whether the user is editing. + * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found. + */ + getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise { + return CoreSitePluginsAssignSubmissionComponent; + } + + /** + * Get a readable name to use for the plugin. + * + * @param {any} plugin The plugin object. + * @return {string} The plugin name. + */ + getPluginName(plugin: any): string { + // Check if there's a translated string for the plugin. + const translationId = this.prefix + 'pluginname', + translation = this.translate.instant(translationId); + + if (translationId != translation) { + // Translation found, use it. + return translation; + } + + // Fallback to WS string. + if (plugin.name) { + return plugin.name; + } + } + + /** + * Whether or not the handler is enabled for edit on a site level. + * + * @return {boolean|Promise} Whether or not the handler is enabled for edit on a site level. + */ + isEnabledForEdit(): boolean | Promise { + return true; + } +} diff --git a/src/core/siteplugins/components/assign-feedback/assign-feedback.html b/src/core/siteplugins/components/assign-feedback/assign-feedback.html new file mode 100644 index 000000000..f58bcd913 --- /dev/null +++ b/src/core/siteplugins/components/assign-feedback/assign-feedback.html @@ -0,0 +1 @@ + diff --git a/src/core/siteplugins/components/assign-feedback/assign-feedback.ts b/src/core/siteplugins/components/assign-feedback/assign-feedback.ts new file mode 100644 index 000000000..870ddc30b --- /dev/null +++ b/src/core/siteplugins/components/assign-feedback/assign-feedback.ts @@ -0,0 +1,67 @@ +// (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 { Component, OnInit, Input } from '@angular/core'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; +import { CoreSitePluginsCompileInitComponent } from '../../classes/compile-init-component'; + +/** + * Component that displays an assign feedback plugin created using a site plugin. + */ +@Component({ + selector: 'core-site-plugins-assign-feedback', + templateUrl: 'assign-feedback.html', +}) +export class CoreSitePluginsAssignFeedbackComponent extends CoreSitePluginsCompileInitComponent implements OnInit { + @Input() assign: any; // The assignment. + @Input() submission: any; // The submission. + @Input() plugin: any; // The plugin object. + @Input() userId: number; // The user ID of the submission. + @Input() configs: any; // The configs for the plugin. + @Input() canEdit: boolean; // Whether the user can edit. + @Input() edit: boolean; // Whether the user is editing. + + constructor(sitePluginsProvider: CoreSitePluginsProvider) { + super(sitePluginsProvider); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Pass the input and output data to the component. + this.jsData = { + assign: this.assign, + submission: this.submission, + plugin: this.plugin, + userId: this.userId, + configs: this.configs, + edit: this.edit, + canEdit: this.canEdit + }; + + if (this.plugin) { + this.getHandlerData('assignfeedback_' + this.plugin.type); + } + } + + /** + * Invalidate the data. + * + * @return {Promise} Promise resolved when done. + */ + invalidate(): Promise { + return Promise.resolve(); + } +} diff --git a/src/core/siteplugins/components/assign-submission/assign-submission.html b/src/core/siteplugins/components/assign-submission/assign-submission.html new file mode 100644 index 000000000..f58bcd913 --- /dev/null +++ b/src/core/siteplugins/components/assign-submission/assign-submission.html @@ -0,0 +1 @@ + diff --git a/src/core/siteplugins/components/assign-submission/assign-submission.ts b/src/core/siteplugins/components/assign-submission/assign-submission.ts new file mode 100644 index 000000000..f01644488 --- /dev/null +++ b/src/core/siteplugins/components/assign-submission/assign-submission.ts @@ -0,0 +1,65 @@ +// (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 { Component, OnInit, Input } from '@angular/core'; +import { CoreSitePluginsProvider } from '../../providers/siteplugins'; +import { CoreSitePluginsCompileInitComponent } from '../../classes/compile-init-component'; + +/** + * Component that displays an assign submission plugin created using a site plugin. + */ +@Component({ + selector: 'core-site-plugins-assign-submission', + templateUrl: 'assign-submission.html', +}) +export class CoreSitePluginsAssignSubmissionComponent extends CoreSitePluginsCompileInitComponent implements OnInit { + @Input() assign: any; // The assignment. + @Input() submission: any; // The submission. + @Input() plugin: any; // The plugin object. + @Input() configs: any; // The configs for the plugin. + @Input() edit: boolean; // Whether the user is editing. + @Input() allowOffline: boolean; // Whether to allow offline. + + constructor(sitePluginsProvider: CoreSitePluginsProvider) { + super(sitePluginsProvider); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + // Pass the input and output data to the component. + this.jsData = { + assign: this.assign, + submission: this.submission, + plugin: this.plugin, + configs: this.configs, + edit: this.edit, + allowOffline: this.allowOffline + }; + + if (this.plugin) { + this.getHandlerData('assignsubmission_' + this.plugin.type); + } + } + + /** + * Invalidate the data. + * + * @return {Promise} Promise resolved when done. + */ + invalidate(): Promise { + return Promise.resolve(); + } +} diff --git a/src/core/siteplugins/components/components.module.ts b/src/core/siteplugins/components/components.module.ts index c83d80db8..a5e5b75a7 100644 --- a/src/core/siteplugins/components/components.module.ts +++ b/src/core/siteplugins/components/components.module.ts @@ -26,6 +26,8 @@ import { CoreSitePluginsUserProfileFieldComponent } from './user-profile-field/u import { CoreSitePluginsQuestionComponent } from './question/question'; import { CoreSitePluginsQuestionBehaviourComponent } from './question-behaviour/question-behaviour'; import { CoreSitePluginsQuizAccessRuleComponent } from './quiz-access-rule/quiz-access-rule'; +import { CoreSitePluginsAssignFeedbackComponent } from './assign-feedback/assign-feedback'; +import { CoreSitePluginsAssignSubmissionComponent } from './assign-submission/assign-submission'; @NgModule({ declarations: [ @@ -36,7 +38,9 @@ import { CoreSitePluginsQuizAccessRuleComponent } from './quiz-access-rule/quiz- CoreSitePluginsUserProfileFieldComponent, CoreSitePluginsQuestionComponent, CoreSitePluginsQuestionBehaviourComponent, - CoreSitePluginsQuizAccessRuleComponent + CoreSitePluginsQuizAccessRuleComponent, + CoreSitePluginsAssignFeedbackComponent, + CoreSitePluginsAssignSubmissionComponent ], imports: [ CommonModule, @@ -55,7 +59,9 @@ import { CoreSitePluginsQuizAccessRuleComponent } from './quiz-access-rule/quiz- CoreSitePluginsUserProfileFieldComponent, CoreSitePluginsQuestionComponent, CoreSitePluginsQuestionBehaviourComponent, - CoreSitePluginsQuizAccessRuleComponent + CoreSitePluginsQuizAccessRuleComponent, + CoreSitePluginsAssignFeedbackComponent, + CoreSitePluginsAssignSubmissionComponent ], entryComponents: [ CoreSitePluginsModuleIndexComponent, @@ -64,7 +70,9 @@ import { CoreSitePluginsQuizAccessRuleComponent } from './quiz-access-rule/quiz- CoreSitePluginsUserProfileFieldComponent, CoreSitePluginsQuestionComponent, CoreSitePluginsQuestionBehaviourComponent, - CoreSitePluginsQuizAccessRuleComponent + CoreSitePluginsQuizAccessRuleComponent, + CoreSitePluginsAssignFeedbackComponent, + CoreSitePluginsAssignSubmissionComponent ] }) export class CoreSitePluginsComponentsModule {} diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index 5a05e7363..da08de28c 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -14,6 +14,7 @@ import { Injectable, Injector } from '@angular/core'; import { Http } from '@angular/http'; +import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreLangProvider } from '@providers/lang'; @@ -40,6 +41,8 @@ import { CoreQuestionDelegate } from '@core/question/providers/delegate'; import { CoreQuestionBehaviourDelegate } from '@core/question/providers/behaviour-delegate'; import { AddonMessageOutputDelegate } from '@addon/messageoutput/providers/delegate'; import { AddonModQuizAccessRuleDelegate } from '@addon/mod/quiz/providers/access-rules-delegate'; +import { AddonModAssignFeedbackDelegate } from '@addon/mod/assign/providers/feedback-delegate'; +import { AddonModAssignSubmissionDelegate } from '@addon/mod/assign/providers/submission-delegate'; // Handler classes. import { CoreSitePluginsCourseFormatHandler } from '../classes/handlers/course-format-handler'; @@ -54,6 +57,8 @@ import { CoreSitePluginsQuestionHandler } from '../classes/handlers/question-han import { CoreSitePluginsQuestionBehaviourHandler } from '../classes/handlers/question-behaviour-handler'; import { CoreSitePluginsMessageOutputHandler } from '../classes/handlers/message-output-handler'; import { CoreSitePluginsQuizAccessRuleHandler } from '../classes/handlers/quiz-access-rule-handler'; +import { CoreSitePluginsAssignFeedbackHandler } from '../classes/handlers/assign-feedback-handler'; +import { CoreSitePluginsAssignSubmissionHandler } from '../classes/handlers/assign-submission-handler'; /** * Helper service to provide functionalities regarding site plugins. It basically has the features to load and register site @@ -78,8 +83,9 @@ export class CoreSitePluginsHelperProvider { private textUtils: CoreTextUtilsProvider, private filepoolProvider: CoreFilepoolProvider, private settingsDelegate: CoreSettingsDelegate, private questionDelegate: CoreQuestionDelegate, private questionBehaviourDelegate: CoreQuestionBehaviourDelegate, private questionProvider: CoreQuestionProvider, - private messageOutputDelegate: AddonMessageOutputDelegate, - private accessRulesDelegate: AddonModQuizAccessRuleDelegate) { + private messageOutputDelegate: AddonMessageOutputDelegate, private accessRulesDelegate: AddonModQuizAccessRuleDelegate, + private assignSubmissionDelegate: AddonModAssignSubmissionDelegate, private translate: TranslateService, + private assignFeedbackDelegate: AddonModAssignFeedbackDelegate) { this.logger = logger.getInstance('CoreSitePluginsHelperProvider'); @@ -469,6 +475,14 @@ export class CoreSitePluginsHelperProvider { promise = Promise.resolve(this.registerQuizAccessRuleHandler(plugin, handlerName, handlerSchema, result)); break; + case 'AddonModAssignFeedbackDelegate': + promise = Promise.resolve(this.registerAssignFeedbackHandler(plugin, handlerName, handlerSchema, result)); + break; + + case 'AddonModAssignSubmissionDelegate': + promise = Promise.resolve(this.registerAssignSubmissionHandler(plugin, handlerName, handlerSchema, result)); + break; + default: // Nothing to do. promise = Promise.resolve(); @@ -490,6 +504,108 @@ export class CoreSitePluginsHelperProvider { }); } + /** + * Given a handler in a plugin, register it in the assign feedback delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} initResult Result of the init WS call. + * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. + */ + protected registerAssignFeedbackHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any) + : string | Promise { + if (!handlerSchema.method) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide method', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in assign feedback delegate:', plugin, handlerSchema, initResult); + + // Execute the main method and its JS. The template returned will be used in the feedback component. + return this.executeMethodAndJS(plugin, handlerSchema.method).then((result) => { + + // Create and register the handler. + const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), + type = plugin.component.replace('assignfeedback_', ''), + prefix = this.getPrefixForStrings(plugin.addon), + feedbackHandler = new CoreSitePluginsAssignFeedbackHandler(this.translate, uniqueName, type, prefix); + + // Store in handlerSchema some data required by the component. + handlerSchema.methodTemplates = result.templates; + handlerSchema.methodJSResult = result.jsResult; + + if (result && result.jsResult) { + // Override default handler functions with the result of the method JS. + for (const property in feedbackHandler) { + if (property != 'constructor' && typeof feedbackHandler[property] == 'function' && + typeof result.jsResult[property] == 'function') { + feedbackHandler[property] = result.jsResult[property].bind(feedbackHandler); + } + } + } + + this.assignFeedbackDelegate.registerHandler(feedbackHandler); + + return plugin.component; + }).catch((err) => { + this.logger.error('Error executing main method for assign feedback', handlerSchema.method, err); + }); + } + + /** + * Given a handler in a plugin, register it in the assign submission delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @param {any} initResult Result of the init WS call. + * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. + */ + protected registerAssignSubmissionHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any) + : string | Promise { + if (!handlerSchema.method) { + // Required data not provided, stop. + this.logger.warn('Ignore site plugin because it doesn\'t provide method', plugin, handlerSchema); + + return; + } + + this.logger.debug('Register site plugin in assign submission delegate:', plugin, handlerSchema, initResult); + + // Execute the main method and its JS. The template returned will be used in the submission component. + return this.executeMethodAndJS(plugin, handlerSchema.method).then((result) => { + + // Create and register the handler. + const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), + type = plugin.component.replace('assignsubmission_', ''), + prefix = this.getPrefixForStrings(plugin.addon), + submissionHandler = new CoreSitePluginsAssignSubmissionHandler(this.translate, uniqueName, type, prefix); + + // Store in handlerSchema some data required by the component. + handlerSchema.methodTemplates = result.templates; + handlerSchema.methodJSResult = result.jsResult; + + if (result && result.jsResult) { + // Override default handler functions with the result of the method JS. + for (const property in submissionHandler) { + if (property != 'constructor' && typeof submissionHandler[property] == 'function' && + typeof result.jsResult[property] == 'function') { + submissionHandler[property] = result.jsResult[property].bind(submissionHandler); + } + } + } + + this.assignSubmissionDelegate.registerHandler(submissionHandler); + + return plugin.component; + }).catch((err) => { + this.logger.error('Error executing main method for assign submission', handlerSchema.method, err); + }); + } + /** * Given a handler in a plugin, register it in the course format delegate. * From 12bab491b8bbae0bdba8cd73e188f95d7461bfe0 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 9 May 2018 16:29:18 +0200 Subject: [PATCH 7/9] MOBILE-2376 siteplugins: Abstract some register functions --- .../user-profile-field/user-profile-field.ts | 2 +- src/core/siteplugins/providers/helper.ts | 279 +++++------------- 2 files changed, 71 insertions(+), 210 deletions(-) diff --git a/src/core/siteplugins/components/user-profile-field/user-profile-field.ts b/src/core/siteplugins/components/user-profile-field/user-profile-field.ts index f5c2271d2..81e5a27f8 100644 --- a/src/core/siteplugins/components/user-profile-field/user-profile-field.ts +++ b/src/core/siteplugins/components/user-profile-field/user-profile-field.ts @@ -49,7 +49,7 @@ export class CoreSitePluginsUserProfileFieldComponent extends CoreSitePluginsCom }; if (this.field) { - this.getHandlerData(this.field.type || this.field.datatype); + this.getHandlerData('profilefield_' + (this.field.type || this.field.datatype)); } } } diff --git a/src/core/siteplugins/providers/helper.ts b/src/core/siteplugins/providers/helper.ts index da08de28c..1a64945e0 100644 --- a/src/core/siteplugins/providers/helper.ts +++ b/src/core/siteplugins/providers/helper.ts @@ -436,7 +436,7 @@ export class CoreSitePluginsHelperProvider { break; case 'CoreCourseModuleDelegate': - promise = Promise.resolve(this.registerModuleHandler(plugin, handlerName, handlerSchema, result)); + promise = Promise.resolve(this.registerModuleHandler(plugin, handlerName, handlerSchema)); break; case 'CoreUserDelegate': @@ -448,11 +448,11 @@ export class CoreSitePluginsHelperProvider { break; case 'CoreCourseFormatDelegate': - promise = Promise.resolve(this.registerCourseFormatHandler(plugin, handlerName, handlerSchema, result)); + promise = Promise.resolve(this.registerCourseFormatHandler(plugin, handlerName, handlerSchema)); break; case 'CoreUserProfileFieldDelegate': - promise = Promise.resolve(this.registerUserProfileFieldHandler(plugin, handlerName, handlerSchema, result)); + promise = Promise.resolve(this.registerUserProfileFieldHandler(plugin, handlerName, handlerSchema)); break; case 'CoreSettingsDelegate': @@ -460,11 +460,11 @@ export class CoreSitePluginsHelperProvider { break; case 'CoreQuestionDelegate': - promise = Promise.resolve(this.registerQuestionHandler(plugin, handlerName, handlerSchema, result)); + promise = Promise.resolve(this.registerQuestionHandler(plugin, handlerName, handlerSchema)); break; case 'CoreQuestionBehaviourDelegate': - promise = Promise.resolve(this.registerQuestionBehaviourHandler(plugin, handlerName, handlerSchema, result)); + promise = Promise.resolve(this.registerQuestionBehaviourHandler(plugin, handlerName, handlerSchema)); break; case 'AddonMessageOutputDelegate': @@ -472,15 +472,15 @@ export class CoreSitePluginsHelperProvider { break; case 'AddonModQuizAccessRuleDelegate': - promise = Promise.resolve(this.registerQuizAccessRuleHandler(plugin, handlerName, handlerSchema, result)); + promise = Promise.resolve(this.registerQuizAccessRuleHandler(plugin, handlerName, handlerSchema)); break; case 'AddonModAssignFeedbackDelegate': - promise = Promise.resolve(this.registerAssignFeedbackHandler(plugin, handlerName, handlerSchema, result)); + promise = Promise.resolve(this.registerAssignFeedbackHandler(plugin, handlerName, handlerSchema)); break; case 'AddonModAssignSubmissionDelegate': - promise = Promise.resolve(this.registerAssignSubmissionHandler(plugin, handlerName, handlerSchema, result)); + promise = Promise.resolve(this.registerAssignSubmissionHandler(plugin, handlerName, handlerSchema)); break; default: @@ -505,16 +505,18 @@ export class CoreSitePluginsHelperProvider { } /** - * Given a handler in a plugin, register it in the assign feedback delegate. + * Register a handler that relies in a "componentInit" function in a certain delegate. + * These type of handlers will return a generic template and its JS in the main method, so it will be called + * before registering the handler. * * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. * @param {any} handlerSchema Data about the handler. - * @param {any} initResult Result of the init WS call. * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. */ - protected registerAssignFeedbackHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any) - : string | Promise { + protected registerComponentInitHandler(plugin: any, handlerName: string, handlerSchema: any, delegate: any, + createHandlerFn: (uniqueName: string, result: any) => any): string | Promise { + if (!handlerSchema.method) { // Required data not provided, stop. this.logger.warn('Ignore site plugin because it doesn\'t provide method', plugin, handlerSchema); @@ -522,16 +524,14 @@ export class CoreSitePluginsHelperProvider { return; } - this.logger.debug('Register site plugin in assign feedback delegate:', plugin, handlerSchema, initResult); + this.logger.debug('Register site plugin', plugin, handlerSchema); - // Execute the main method and its JS. The template returned will be used in the feedback component. + // Execute the main method and its JS. The template returned will be used in the right component. return this.executeMethodAndJS(plugin, handlerSchema.method).then((result) => { // Create and register the handler. const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), - type = plugin.component.replace('assignfeedback_', ''), - prefix = this.getPrefixForStrings(plugin.addon), - feedbackHandler = new CoreSitePluginsAssignFeedbackHandler(this.translate, uniqueName, type, prefix); + handler = createHandlerFn(uniqueName, result); // Store in handlerSchema some data required by the component. handlerSchema.methodTemplates = result.templates; @@ -539,19 +539,39 @@ export class CoreSitePluginsHelperProvider { if (result && result.jsResult) { // Override default handler functions with the result of the method JS. - for (const property in feedbackHandler) { - if (property != 'constructor' && typeof feedbackHandler[property] == 'function' && + for (const property in handler) { + if (property != 'constructor' && typeof handler[property] == 'function' && typeof result.jsResult[property] == 'function') { - feedbackHandler[property] = result.jsResult[property].bind(feedbackHandler); + handler[property] = result.jsResult[property].bind(handler); } } } - this.assignFeedbackDelegate.registerHandler(feedbackHandler); + delegate.registerHandler(handler); return plugin.component; }).catch((err) => { - this.logger.error('Error executing main method for assign feedback', handlerSchema.method, err); + this.logger.error('Error executing main method', plugin.component, handlerSchema.method, err); + }); + } + + /** + * Given a handler in a plugin, register it in the assign feedback delegate. + * + * @param {any} plugin Data of the plugin. + * @param {string} handlerName Name of the handler in the plugin. + * @param {any} handlerSchema Data about the handler. + * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. + */ + protected registerAssignFeedbackHandler(plugin: any, handlerName: string, handlerSchema: any): string | Promise { + + return this.registerComponentInitHandler(plugin, handlerName, handlerSchema, this.assignFeedbackDelegate, + (uniqueName: string, result: any) => { + + const type = plugin.component.replace('assignfeedback_', ''), + prefix = this.getPrefixForStrings(plugin.addon); + + return new CoreSitePluginsAssignFeedbackHandler(this.translate, uniqueName, type, prefix); }); } @@ -561,48 +581,17 @@ export class CoreSitePluginsHelperProvider { * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. * @param {any} handlerSchema Data about the handler. - * @param {any} initResult Result of the init WS call. * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. */ - protected registerAssignSubmissionHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any) - : string | Promise { - if (!handlerSchema.method) { - // Required data not provided, stop. - this.logger.warn('Ignore site plugin because it doesn\'t provide method', plugin, handlerSchema); + protected registerAssignSubmissionHandler(plugin: any, handlerName: string, handlerSchema: any): string | Promise { - return; - } + return this.registerComponentInitHandler(plugin, handlerName, handlerSchema, this.assignSubmissionDelegate, + (uniqueName: string, result: any) => { - this.logger.debug('Register site plugin in assign submission delegate:', plugin, handlerSchema, initResult); + const type = plugin.component.replace('assignsubmission_', ''), + prefix = this.getPrefixForStrings(plugin.addon); - // Execute the main method and its JS. The template returned will be used in the submission component. - return this.executeMethodAndJS(plugin, handlerSchema.method).then((result) => { - - // Create and register the handler. - const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), - type = plugin.component.replace('assignsubmission_', ''), - prefix = this.getPrefixForStrings(plugin.addon), - submissionHandler = new CoreSitePluginsAssignSubmissionHandler(this.translate, uniqueName, type, prefix); - - // Store in handlerSchema some data required by the component. - handlerSchema.methodTemplates = result.templates; - handlerSchema.methodJSResult = result.jsResult; - - if (result && result.jsResult) { - // Override default handler functions with the result of the method JS. - for (const property in submissionHandler) { - if (property != 'constructor' && typeof submissionHandler[property] == 'function' && - typeof result.jsResult[property] == 'function') { - submissionHandler[property] = result.jsResult[property].bind(submissionHandler); - } - } - } - - this.assignSubmissionDelegate.registerHandler(submissionHandler); - - return plugin.component; - }).catch((err) => { - this.logger.error('Error executing main method for assign submission', handlerSchema.method, err); + return new CoreSitePluginsAssignSubmissionHandler(this.translate, uniqueName, type, prefix); }); } @@ -612,11 +601,10 @@ export class CoreSitePluginsHelperProvider { * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. * @param {any} handlerSchema Data about the handler. - * @param {any} initResult Result of the init WS call. * @return {string} A string to identify the handler. */ - protected registerCourseFormatHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any): string { - this.logger.debug('Register site plugin in course format delegate:', plugin, handlerSchema, initResult); + protected registerCourseFormatHandler(plugin: any, handlerName: string, handlerSchema: any): string { + this.logger.debug('Register site plugin in course format delegate:', plugin, handlerSchema); // Create and register the handler. const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), @@ -720,10 +708,9 @@ export class CoreSitePluginsHelperProvider { * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. * @param {any} handlerSchema Data about the handler. - * @param {any} initResult Result of the init WS call. * @return {string} A string to identify the handler. */ - protected registerModuleHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any): string { + protected registerModuleHandler(plugin: any, handlerName: string, handlerSchema: any): string { if (!handlerSchema.displaydata) { // Required data not provided, stop. this.logger.warn('Ignore site plugin because it doesn\'t provide displaydata', plugin, handlerSchema); @@ -731,7 +718,7 @@ export class CoreSitePluginsHelperProvider { return; } - this.logger.debug('Register site plugin in module delegate:', plugin, handlerSchema, initResult); + this.logger.debug('Register site plugin in module delegate:', plugin, handlerSchema); // Create and register the handler. const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), @@ -754,46 +741,14 @@ export class CoreSitePluginsHelperProvider { * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. * @param {any} handlerSchema Data about the handler. - * @param {any} initResult Result of the init WS call. * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. */ - protected registerQuestionHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any) - : string | Promise { - if (!handlerSchema.method) { - // Required data not provided, stop. - this.logger.warn('Ignore site plugin because it doesn\'t provide method', plugin, handlerSchema); + protected registerQuestionHandler(plugin: any, handlerName: string, handlerSchema: any): string | Promise { - return; - } + return this.registerComponentInitHandler(plugin, handlerName, handlerSchema, this.questionDelegate, + (uniqueName: string, result: any) => { - this.logger.debug('Register site plugin in question delegate:', plugin, handlerSchema, initResult); - - // Execute the main method and its JS. The template returned will be used in the question component. - return this.executeMethodAndJS(plugin, handlerSchema.method).then((result) => { - // Create and register the handler. - const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), - questionType = plugin.component, - questionHandler = new CoreSitePluginsQuestionHandler(uniqueName, questionType); - - // Store in handlerSchema some data required by the component. - handlerSchema.methodTemplates = result.templates; - handlerSchema.methodJSResult = result.jsResult; - - if (result && result.jsResult) { - // Override default handler functions with the result of the method JS. - for (const property in questionHandler) { - if (property != 'constructor' && typeof questionHandler[property] == 'function' && - typeof result.jsResult[property] == 'function') { - questionHandler[property] = result.jsResult[property].bind(questionHandler); - } - } - } - - this.questionDelegate.registerHandler(questionHandler); - - return questionType; - }).catch((err) => { - this.logger.error('Error executing main method for question', handlerSchema.method, err); + return new CoreSitePluginsQuestionHandler(uniqueName, plugin.component); }); } @@ -803,48 +758,16 @@ export class CoreSitePluginsHelperProvider { * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. * @param {any} handlerSchema Data about the handler. - * @param {any} initResult Result of the init WS call. * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. */ - protected registerQuestionBehaviourHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any) - : string | Promise { - if (!handlerSchema.method) { - // Required data not provided, stop. - this.logger.warn('Ignore site plugin because it doesn\'t provide method', plugin, handlerSchema); + protected registerQuestionBehaviourHandler(plugin: any, handlerName: string, handlerSchema: any): string | Promise { - return; - } + return this.registerComponentInitHandler(plugin, handlerName, handlerSchema, this.questionBehaviourDelegate, + (uniqueName: string, result: any) => { - this.logger.debug('Register site plugin in question behaviour delegate:', plugin, handlerSchema, initResult); + const type = plugin.component.replace('qbehaviour_', ''); - // Execute the main method and its JS. The template returned will be used in the question component. - return this.executeMethodAndJS(plugin, handlerSchema.method).then((result) => { - - // Create and register the handler. - const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), - type = plugin.component.replace('qbehaviour_', ''), - behaviourHandler = new CoreSitePluginsQuestionBehaviourHandler(this.questionProvider, uniqueName, type, - result.templates.length); - - // Store in handlerSchema some data required by the component. - handlerSchema.methodTemplates = result.templates; - handlerSchema.methodJSResult = result.jsResult; - - if (result && result.jsResult) { - // Override default handler functions with the result of the method JS. - for (const property in behaviourHandler) { - if (property != 'constructor' && typeof behaviourHandler[property] == 'function' && - typeof result.jsResult[property] == 'function') { - behaviourHandler[property] = result.jsResult[property].bind(behaviourHandler); - } - } - } - - this.questionBehaviourDelegate.registerHandler(behaviourHandler); - - return plugin.component; - }).catch((err) => { - this.logger.error('Error executing main method for question behaviour', handlerSchema.method, err); + return new CoreSitePluginsQuestionBehaviourHandler(this.questionProvider, uniqueName, type, result.templates.length); }); } @@ -854,46 +777,14 @@ export class CoreSitePluginsHelperProvider { * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. * @param {any} handlerSchema Data about the handler. - * @param {any} initResult Result of the init WS call. * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. */ - protected registerQuizAccessRuleHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any) - : string | Promise { - if (!handlerSchema.method) { - // Required data not provided, stop. - this.logger.warn('Ignore site plugin because it doesn\'t provide method', plugin, handlerSchema); + protected registerQuizAccessRuleHandler(plugin: any, handlerName: string, handlerSchema: any): string | Promise { - return; - } + return this.registerComponentInitHandler(plugin, handlerName, handlerSchema, this.accessRulesDelegate, + (uniqueName: string, result: any) => { - this.logger.debug('Register site plugin in quiz access rule delegate:', plugin, handlerSchema, initResult); - - // Execute the main method and its JS. The template returned will be used in the access rule component. - return this.executeMethodAndJS(plugin, handlerSchema.method).then((result) => { - // Create and register the handler. - const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), - ruleName = plugin.component, - ruleHandler = new CoreSitePluginsQuizAccessRuleHandler(uniqueName, ruleName, result.templates.length); - - // Store in handlerSchema some data required by the component. - handlerSchema.methodTemplates = result.templates; - handlerSchema.methodJSResult = result.jsResult; - - if (result && result.jsResult) { - // Override default handler functions with the result of the method JS. - for (const property in ruleHandler) { - if (property != 'constructor' && typeof ruleHandler[property] == 'function' && - typeof result.jsResult[property] == 'function') { - ruleHandler[property] = result.jsResult[property].bind(ruleHandler); - } - } - } - - this.accessRulesDelegate.registerHandler(ruleHandler); - - return ruleName; - }).catch((err) => { - this.logger.error('Error executing main method for quiz access rule', handlerSchema.method, err); + return new CoreSitePluginsQuizAccessRuleHandler(uniqueName, plugin.component, result.templates.length); }); } @@ -961,46 +852,16 @@ export class CoreSitePluginsHelperProvider { * @param {any} plugin Data of the plugin. * @param {string} handlerName Name of the handler in the plugin. * @param {any} handlerSchema Data about the handler. - * @param {any} initResult Result of the init WS call. * @return {string|Promise} A string (or a promise resolved with a string) to identify the handler. */ - protected registerUserProfileFieldHandler(plugin: any, handlerName: string, handlerSchema: any, initResult: any) - : string | Promise { - if (!handlerSchema.method) { - // Required data not provided, stop. - this.logger.warn('Ignore site plugin because it doesn\'t provide method', plugin, handlerSchema); + protected registerUserProfileFieldHandler(plugin: any, handlerName: string, handlerSchema: any): string | Promise { - return; - } + return this.registerComponentInitHandler(plugin, handlerName, handlerSchema, this.profileFieldDelegate, + (uniqueName: string, result: any) => { - this.logger.debug('Register site plugin in user profile field delegate:', plugin, handlerSchema, initResult); + const fieldType = plugin.component.replace('profilefield_', ''); - // Execute the main method and its JS. The template returned will be used in the profile field component. - return this.executeMethodAndJS(plugin, handlerSchema.method).then((result) => { - // Create and register the handler. - const uniqueName = this.sitePluginsProvider.getHandlerUniqueName(plugin, handlerName), - fieldType = plugin.component.replace('profilefield_', ''), - fieldHandler = new CoreSitePluginsUserProfileFieldHandler(uniqueName, fieldType); - - // Store in handlerSchema some data required by the component. - handlerSchema.methodTemplates = result.templates; - handlerSchema.methodJSResult = result.jsResult; - - if (result && result.jsResult) { - // Override default handler functions with the result of the method JS. - for (const property in fieldHandler) { - if (property != 'constructor' && typeof fieldHandler[property] == 'function' && - typeof result.jsResult[property] == 'function') { - fieldHandler[property] = result.jsResult[property].bind(fieldHandler); - } - } - } - - this.profileFieldDelegate.registerHandler(fieldHandler); - - return fieldType; - }).catch((err) => { - this.logger.error('Error executing main method for user profile field', handlerSchema.method, err); + return new CoreSitePluginsUserProfileFieldHandler(uniqueName, fieldType); }); } } From e1cb1ad7ecbd57ea53277f05c02d53349316ce1c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 10 May 2018 09:19:59 +0200 Subject: [PATCH 8/9] MOBILE-2376 siteplugins: Limit main menu plugins priority --- src/core/siteplugins/classes/handlers/main-menu-handler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/siteplugins/classes/handlers/main-menu-handler.ts b/src/core/siteplugins/classes/handlers/main-menu-handler.ts index d837d7525..09dc3c188 100644 --- a/src/core/siteplugins/classes/handlers/main-menu-handler.ts +++ b/src/core/siteplugins/classes/handlers/main-menu-handler.ts @@ -25,7 +25,8 @@ export class CoreSitePluginsMainMenuHandler extends CoreSitePluginsBaseHandler i protected initResult: any) { super(name); - this.priority = handlerSchema.priority; + // Set 699 as max priority so site plugins are always shown in the More tab (700 is Notifications tab). + this.priority = Math.min(handlerSchema.priority, 699); } /** From ce62a16460f63faec370e306f78a7293b9db0b6a Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 7 May 2018 10:08:24 +0200 Subject: [PATCH 9/9] MOBILE-2376 core: Decrease exceptions in console on start --- .../messages/providers/mainmenu-handler.ts | 2 +- src/addon/messages/providers/sync.ts | 9 ++-- .../providers/pushnotifications.ts | 5 +++ .../remotethemes/providers/remotethemes.ts | 45 ++++++++++++------- src/providers/cron.ts | 8 ++-- src/providers/file.ts | 4 +- src/providers/sites.ts | 2 + 7 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/addon/messages/providers/mainmenu-handler.ts b/src/addon/messages/providers/mainmenu-handler.ts index f934224be..2fde797bf 100644 --- a/src/addon/messages/providers/mainmenu-handler.ts +++ b/src/addon/messages/providers/mainmenu-handler.ts @@ -129,7 +129,7 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr */ execute(siteId?: string): Promise { if (this.sitesProvider.isCurrentSite(siteId)) { - this.eventsProvider.trigger(AddonMessagesProvider.READ_CRON_EVENT, undefined, siteId); + this.eventsProvider.trigger(AddonMessagesProvider.READ_CRON_EVENT, {}, siteId); } if (this.appProvider.isDesktop() && this.localNotificationsProvider.isAvailable()) { diff --git a/src/addon/messages/providers/sync.ts b/src/addon/messages/providers/sync.ts index 0cc5a6c5b..0a9b77ac7 100644 --- a/src/addon/messages/providers/sync.ts +++ b/src/addon/messages/providers/sync.ts @@ -58,11 +58,12 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { /** * Get all messages pending to be sent in the site. - * @param {boolean} [onlyDeviceOffline=false] True to only sync discussions that failed because device was offline, - * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. - * @param {Promise} Promise resolved if sync is successful, rejected if sync fails. + * + * @param {string} [siteId] Site ID to sync. If not defined, sync all sites. + * @param {boolean} [onlyDeviceOffline=false] True to only sync discussions that failed because device was offline. + * @param {Promise} Promise resolved if sync is successful, rejected if sync fails. */ - protected syncAllDiscussionsFunc(onlyDeviceOffline: boolean = false, siteId?: string): Promise { + protected syncAllDiscussionsFunc(siteId?: string, onlyDeviceOffline: boolean = false): Promise { const promise = onlyDeviceOffline ? this.messagesOffline.getAllDeviceOfflineMessages(siteId) : this.messagesOffline.getAllMessages(siteId); diff --git a/src/addon/pushnotifications/providers/pushnotifications.ts b/src/addon/pushnotifications/providers/pushnotifications.ts index 98e1e266e..74bb4ec75 100644 --- a/src/addon/pushnotifications/providers/pushnotifications.ts +++ b/src/addon/pushnotifications/providers/pushnotifications.ts @@ -264,6 +264,11 @@ export class AddonPushNotificationsProvider { return previous + parseInt(counter, 10); }, 0); + if (!this.appProvider.isDesktop() && !this.appProvider.isMobile()) { + // Browser doesn't have an app badge, stop. + return total; + } + // Set the app badge. return this.badge.set(total).then(() => { return total; diff --git a/src/addon/remotethemes/providers/remotethemes.ts b/src/addon/remotethemes/providers/remotethemes.ts index 067bca557..ceebbf659 100644 --- a/src/addon/remotethemes/providers/remotethemes.ts +++ b/src/addon/remotethemes/providers/remotethemes.ts @@ -133,24 +133,26 @@ export class AddonRemoteThemesProvider { * Get remote styles of a certain site. * * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise<{fileUrl: string, styles: string}>} Promise resolved with the styles and the URL of the CSS file. + * @return {Promise<{fileUrl: string, styles: string}>} Promise resolved with the styles and the URL of the CSS file, + * resolved with undefined if no styles to load. */ get(siteId?: string): Promise<{fileUrl: string, styles: string}> { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - let fileUrl; - return this.sitesProvider.getSite(siteId).then((site) => { const infos = site.getInfo(); + let promise, + fileUrl; + if (infos && infos.mobilecssurl) { fileUrl = infos.mobilecssurl; if (this.fileProvider.isAvailable()) { // The file system is available. Download the file and remove old CSS files if needed. - return this.downloadFileAndRemoveOld(siteId, fileUrl); + promise = this.downloadFileAndRemoveOld(siteId, fileUrl); } else { // Return the online URL. - return fileUrl; + promise = Promise.resolve(fileUrl); } } else { if (infos && infos.mobilecssurl === '') { @@ -158,20 +160,22 @@ export class AddonRemoteThemesProvider { this.filepoolProvider.removeFilesByComponent(siteId, AddonRemoteThemesProvider.COMPONENT, 1); } - return Promise.reject(null); + return; } - }).then((url) => { - this.logger.debug('Loading styles from: ', url); - // Get the CSS content using HTTP because we will treat the styles before saving them in the file. - return this.http.get(url).toPromise(); - }).then((response): any => { - const text = response && response.text(); - if (typeof text == 'string') { - return {fileUrl: fileUrl, styles: this.get35Styles(text)}; - } else { - return Promise.reject(null); - } + return promise.then((url) => { + this.logger.debug('Loading styles from: ', url); + + // Get the CSS content using HTTP because we will treat the styles before saving them in the file. + return this.http.get(url).toPromise(); + }).then((response): any => { + const text = response && response.text(); + if (typeof text == 'string') { + return {fileUrl: fileUrl, styles: this.get35Styles(text)}; + } else { + return Promise.reject(null); + } + }); }); } @@ -208,6 +212,11 @@ export class AddonRemoteThemesProvider { this.disableElement(this.stylesEls[siteId].element, disabled); return this.get(siteId).then((data) => { + if (typeof data == 'undefined') { + // Nothing to load. + return; + } + const hash = Md5.hashAsciiStr(data.styles); // Update the styles only if they have changed. @@ -271,6 +280,8 @@ export class AddonRemoteThemesProvider { preloadCurrentSite(): Promise { return this.sitesProvider.getStoredCurrentSiteId().then((siteId) => { return this.addSite(siteId); + }, () => { + // No current site stored. }); } diff --git a/src/providers/cron.ts b/src/providers/cron.ts index 4aa968ded..d891d44b2 100644 --- a/src/providers/cron.ts +++ b/src/providers/cron.ts @@ -184,9 +184,9 @@ export class CoreCronDelegate { return this.setHandlerLastExecutionTime(name, Date.now()).then(() => { this.scheduleNextExecution(name); }); - }, () => { + }, (error) => { // Handler call failed. Retry soon. - this.logger.debug(`Execution of handler '${name}' failed.`); + this.logger.error(`Execution of handler '${name}' failed.`, error); this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL); return Promise.reject(null); @@ -440,7 +440,9 @@ export class CoreCronDelegate { this.handlers[name].timeout = setTimeout(() => { delete this.handlers[name].timeout; - this.checkAndExecuteHandler(name); + this.checkAndExecuteHandler(name).catch(() => { + // Ignore errors. + }); }, nextExecution); }); } diff --git a/src/providers/file.ts b/src/providers/file.ts index 836685ab6..9677ab62f 100644 --- a/src/providers/file.ts +++ b/src/providers/file.ts @@ -918,7 +918,9 @@ export class CoreFileProvider { * @return {Promise} Promise resolved when done. */ clearTmpFolder(): Promise { - return this.removeDir(CoreFileProvider.TMPFOLDER); + return this.removeDir(CoreFileProvider.TMPFOLDER).catch(() => { + // Ignore errors because the folder might not exist. + }); } /** diff --git a/src/providers/sites.ts b/src/providers/sites.ts index 8b9694b15..2c087c955 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -975,6 +975,8 @@ export class CoreSitesProvider { this.logger.debug(`Restore session in site ${siteId}`); return this.loadSite(siteId); + }).catch(() => { + // No current session. }); }