diff --git a/src/addon/mod/lti/components/components.module.ts b/src/addon/mod/lti/components/components.module.ts new file mode 100644 index 000000000..d734384b9 --- /dev/null +++ b/src/addon/mod/lti/components/components.module.ts @@ -0,0 +1,45 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreCourseComponentsModule } from '@core/course/components/components.module'; +import { AddonModLtiIndexComponent } from './index/index'; + +@NgModule({ + declarations: [ + AddonModLtiIndexComponent, + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreCourseComponentsModule + ], + providers: [ + ], + exports: [ + AddonModLtiIndexComponent, + ], + entryComponents: [ + AddonModLtiIndexComponent, + ] +}) +export class AddonModLtiComponentsModule {} diff --git a/src/addon/mod/lti/components/index/index.html b/src/addon/mod/lti/components/index/index.html new file mode 100644 index 000000000..e80a89554 --- /dev/null +++ b/src/addon/mod/lti/components/index/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + +
+ +
+ + {{ 'addon.mod_lti.errorinvalidlaunchurl' | translate }} +
diff --git a/src/addon/mod/lti/components/index/index.ts b/src/addon/mod/lti/components/index/index.ts new file mode 100644 index 000000000..4c81f8786 --- /dev/null +++ b/src/addon/mod/lti/components/index/index.ts @@ -0,0 +1,118 @@ +// (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, Optional, Injector } from '@angular/core'; +import { Content } from 'ionic-angular'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; +import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; +import { AddonModLtiProvider } from '../../providers/lti'; + +/** + * Component that displays an LTI entry page. + */ +@Component({ + selector: 'addon-mod-lti-index', + templateUrl: 'index.html', +}) +export class AddonModLtiIndexComponent extends CoreCourseModuleMainActivityComponent { + component = AddonModLtiProvider.COMPONENT; + moduleName = 'lti'; + + lti: any; // The LTI object. + isValidUrl: boolean; + + protected fetchContentDefaultError = 'addon.mod_lti.errorgetlti'; + + constructor(injector: Injector, + @Optional() protected content: Content, + private ltiProvider: AddonModLtiProvider, + private urlUtils: CoreUrlUtilsProvider) { + super(injector, content); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + + this.loadContent(false, true); + } + + /** + * Check the completion. + */ + protected checkCompletion(): void { + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + } + + /** + * Get the LTI data. + * + * @param {boolean} [refresh=false] If it's refreshing content. + * @param {boolean} [sync=false] If the refresh is needs syncing. + * @param {boolean} [showErrors=false] If show errors to the user of hide them. + * @return {Promise} Promise resolved when done. + */ + protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + return this.ltiProvider.getLti(this.courseId, this.module.id).then((ltiData) => { + this.lti = ltiData; + + return this.ltiProvider.getLtiLaunchData(ltiData.id).then((launchData) => { + this.lti.launchdata = launchData; + this.description = this.lti.intro || this.description; + this.isValidUrl = this.urlUtils.isHttpURL(launchData.endpoint); + this.dataRetrieved.emit(this.lti); + }); + }).then(() => { + // All data obtained, now fill the context menu. + this.fillContextMenu(refresh); + }); + } + + /** + * Perform the invalidate content function. + * + * @return {Promise} Resolved when done. + */ + protected invalidateContent(): Promise { + const promises = []; + + promises.push(this.ltiProvider.invalidateLti(this.courseId)); + if (this.lti) { + promises.push(this.ltiProvider.invalidateLtiLaunchData(this.lti.id)); + } + + return Promise.all(promises); + } + + /** + * Launch the LTI. + */ + launch(): void { + // "View" LTI. + this.ltiProvider.logView(this.lti.id).then(() => { + this.checkCompletion(); + }).catch((error) => { + // Ignore errors. + }); + + // Launch LTI. + this.ltiProvider.launch(this.lti.launchdata.endpoint, this.lti.launchdata.parameters).catch((message) => { + if (message) { + this.domUtils.showErrorModal(message); + } + }); + } +} diff --git a/src/addon/mod/lti/lang/en.json b/src/addon/mod/lti/lang/en.json new file mode 100644 index 000000000..3dc7ad1a5 --- /dev/null +++ b/src/addon/mod/lti/lang/en.json @@ -0,0 +1,5 @@ +{ + "errorgetlti": "Error getting module data.", + "errorinvalidlaunchurl": "The launch URL is not valid.", + "launchactivity": "Launch the activity" +} \ No newline at end of file diff --git a/src/addon/mod/lti/lti.module.ts b/src/addon/mod/lti/lti.module.ts new file mode 100644 index 000000000..557775b5c --- /dev/null +++ b/src/addon/mod/lti/lti.module.ts @@ -0,0 +1,41 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { AddonModLtiComponentsModule } from './components/components.module'; +import { AddonModLtiModuleHandler } from './providers/module-handler'; +import { AddonModLtiProvider } from './providers/lti'; +import { AddonModLtiLinkHandler } from './providers/link-handler'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; +import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; + +@NgModule({ + declarations: [ + ], + imports: [ + AddonModLtiComponentsModule + ], + providers: [ + AddonModLtiProvider, + AddonModLtiModuleHandler, + AddonModLtiLinkHandler, + ] +}) +export class AddonModLtiModule { + constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModLtiModuleHandler, + contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModLtiLinkHandler) { + moduleDelegate.registerHandler(moduleHandler); + contentLinksDelegate.registerHandler(linkHandler); + } +} diff --git a/src/addon/mod/lti/pages/index/index.html b/src/addon/mod/lti/pages/index/index.html new file mode 100644 index 000000000..456646402 --- /dev/null +++ b/src/addon/mod/lti/pages/index/index.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/addon/mod/lti/pages/index/index.module.ts b/src/addon/mod/lti/pages/index/index.module.ts new file mode 100644 index 000000000..dda226660 --- /dev/null +++ b/src/addon/mod/lti/pages/index/index.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModLtiComponentsModule } from '../../components/components.module'; +import { AddonModLtiIndexPage } from './index'; + +@NgModule({ + declarations: [ + AddonModLtiIndexPage, + ], + imports: [ + CoreDirectivesModule, + AddonModLtiComponentsModule, + IonicPageModule.forChild(AddonModLtiIndexPage), + TranslateModule.forChild() + ], +}) +export class AddonModLtiIndexPageModule {} diff --git a/src/addon/mod/lti/pages/index/index.ts b/src/addon/mod/lti/pages/index/index.ts new file mode 100644 index 000000000..ea6ab0034 --- /dev/null +++ b/src/addon/mod/lti/pages/index/index.ts @@ -0,0 +1,48 @@ +// (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, ViewChild } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { AddonModLtiIndexComponent } from '../../components/index/index'; + +/** + * Page that displays an LTI. + */ +@IonicPage({ segment: 'addon-mod-lti-index' }) +@Component({ + selector: 'page-addon-mod-lti-index', + templateUrl: 'index.html', +}) +export class AddonModLtiIndexPage { + @ViewChild(AddonModLtiIndexComponent) ltiComponent: AddonModLtiIndexComponent; + + title: string; + module: any; + courseId: number; + + constructor(navParams: NavParams) { + this.module = navParams.get('module') || {}; + this.courseId = navParams.get('courseId'); + this.title = this.module.name; + } + + /** + * Update some data based on the LTI instance. + * + * @param {any} lti LTI instance. + */ + updateData(lti: any): void { + this.title = lti.name || this.title; + } +} diff --git a/src/addon/mod/lti/providers/link-handler.ts b/src/addon/mod/lti/providers/link-handler.ts new file mode 100644 index 000000000..2c69182cb --- /dev/null +++ b/src/addon/mod/lti/providers/link-handler.ts @@ -0,0 +1,29 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; + +/** + * Handler to treat links to LTI. + */ +@Injectable() +export class AddonModLtiLinkHandler extends CoreContentLinksModuleIndexHandler { + name = 'AddonModLtiLinkHandler'; + + constructor(courseHelper: CoreCourseHelperProvider) { + super(courseHelper, 'AddonModLti', 'lti'); + } +} diff --git a/src/addon/mod/lti/providers/lti.ts b/src/addon/mod/lti/providers/lti.ts new file mode 100644 index 000000000..be87ee4ac --- /dev/null +++ b/src/addon/mod/lti/providers/lti.ts @@ -0,0 +1,216 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreFileProvider } from '@providers/file'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreUrlUtilsProvider } from '@providers/utils/url'; + +export interface AddonModLtiParam { + name: string; + value: string; +} + +/** + * Service that provides some features for LTI. + */ +@Injectable() +export class AddonModLtiProvider { + static COMPONENT = 'mmaModLti'; + + protected ROOT_CACHE_KEY = 'mmaModLti:'; + protected LAUNCHER_FILE_NAME = 'lti_launcher.html'; + + constructor(private fileProvider: CoreFileProvider, + private sitesProvider: CoreSitesProvider, + private textUtils: CoreTextUtilsProvider, + private urlUtils: CoreUrlUtilsProvider, + private utils: CoreUtilsProvider, + private translate: TranslateService) {} + + /** + * Delete launcher. + * + * @return {Promise} Promise resolved when the launcher file is deleted. + */ + deleteLauncher(): Promise { + return this.fileProvider.removeFile(this.LAUNCHER_FILE_NAME); + } + + /** + * Generates a launcher file. + * + * @param {string} url Launch URL. + * @param {AddonModLtiParam[]} params Launch params. + * @return {Promise} Promise resolved with the file URL. + */ + generateLauncher(url: string, params: AddonModLtiParam[]): Promise { + if (!this.fileProvider.isAvailable()) { + return Promise.resolve(url); + } + + // Generate a form with the params. + let text = '
\n'; + params.forEach((p) => { + if (p.name == 'ext_submit') { + text += ' \n'; + }); + text += '
\n'; + + // Add an in-line script to automatically submit the form. + text += ' \n'; + + return this.fileProvider.writeFile(this.LAUNCHER_FILE_NAME, text).then((entry) => { + return entry.toURL(); + }); + } + + /** + * Get a LTI. + * + * @param {number} courseId Course ID. + * @param {number} cmId Course module ID. + * @return {Promise} Promise resolved when the LTI is retrieved. + */ + getLti(courseId: number, cmId: number): Promise { + const params: any = { + courseids: [courseId] + }; + const preSets: any = { + cacheKey: this.getLtiCacheKey(courseId) + }; + + return this.sitesProvider.getCurrentSite().read('mod_lti_get_ltis_by_courses', params, preSets).then((response) => { + if (response.ltis) { + const currentLti = response.ltis.find((lti) => lti.coursemodule == cmId); + if (currentLti) { + return currentLti; + } + } + + return Promise.reject(null); + }); + } + + /** + * Get cache key for LTI data WS calls. + * + * @param {number} courseId Course ID. + * @return {string} Cache key. + */ + protected getLtiCacheKey(courseId: number): string { + return this.ROOT_CACHE_KEY + 'lti:' + courseId; + } + + /** + * Get a LTI launch data. + * + * @param {number} id LTI id. + * @return {Promise} Promise resolved when the launch data is retrieved. + */ + getLtiLaunchData(id: number): Promise { + const params: any = { + toolid: id + }; + const preSets = { + cacheKey: this.getLtiLaunchDataCacheKey(id) + }; + + return this.sitesProvider.getCurrentSite().read('mod_lti_get_tool_launch_data', params, preSets).then((response) => { + if (response.endpoint) { + return response; + } + + return Promise.reject(null); + }); + } + + /** + * Get cache key for LTI launch data WS calls. + * + * @param {number} id LTI id. + * @return {string} Cache key. + */ + protected getLtiLaunchDataCacheKey(id: number): string { + return this.ROOT_CACHE_KEY + 'launch:' + id; + } + + /** + * Invalidates LTI data. + * + * @param {number} courseId Course ID. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateLti(courseId: number): Promise { + return this.sitesProvider.getCurrentSite().invalidateWsCacheForKey(this.getLtiCacheKey(courseId)); + } + + /** + * Invalidates options. + * + * @param {number} id LTI id. + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateLtiLaunchData(id: number): Promise { + return this.sitesProvider.getCurrentSite().invalidateWsCacheForKey(this.getLtiLaunchDataCacheKey(id)); + } + + /** + * Launch LTI. + * + * @param {string} url Launch URL. + * @param {AddonModLtiParam[]} params Launch params. + * @return {Promise} Promise resolved when the WS call is successful. + */ + launch(url: string, params: AddonModLtiParam[]): Promise { + if (!this.urlUtils.isHttpURL(url)) { + return Promise.reject(this.translate.instant('addon.mod_lti.errorinvalidlaunchurl')); + } + + // Generate launcher and open it. + return this.generateLauncher(url, params).then((url) => { + this.utils.openInApp(url).show(); + }); + } + + /** + * Report the LTI as being viewed. + * + * @param {string} id LTI id. + * @return {Promise} Promise resolved when the WS call is successful. + */ + logView(id: string): Promise { + if (id) { + const params: any = { + ltiid: id + }; + + return this.sitesProvider.getCurrentSite().write('mod_lti_view_lti', params); + } + + return Promise.reject(null); + } +} diff --git a/src/addon/mod/lti/providers/module-handler.ts b/src/addon/mod/lti/providers/module-handler.ts new file mode 100644 index 000000000..dfeff3028 --- /dev/null +++ b/src/addon/mod/lti/providers/module-handler.ts @@ -0,0 +1,126 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { NavController, NavOptions } from 'ionic-angular'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; +import { CoreAppProvider } from '@providers/app'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreSitesProvider } from '@providers/sites'; +import { AddonModLtiIndexComponent } from '../components/index/index'; +import { AddonModLtiProvider } from './lti'; + +/** + * Handler to support LTI modules. + */ +@Injectable() +export class AddonModLtiModuleHandler implements CoreCourseModuleHandler { + name = 'AddonModLti'; + modName = 'lti'; + + constructor(private appProvider: CoreAppProvider, + private courseProvider: CoreCourseProvider, + private domUtils: CoreDomUtilsProvider, + private filepoolProvider: CoreFilepoolProvider, + private sitesProvider: CoreSitesProvider, + private ltiProvider: AddonModLtiProvider) {} + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean|Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return true; + } + + /** + * Get the data required to display the module in the course contents view. + * + * @param {any} module The module object. + * @param {number} courseId The course ID. + * @param {number} sectionId The section ID. + * @return {CoreCourseModuleHandlerData} Data to render the module. + */ + getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData { + const data: CoreCourseModuleHandlerData = { + icon: this.courseProvider.getModuleIconSrc('lti'), + title: module.name, + class: 'addon-mod_lti-handler', + action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { + navCtrl.push('AddonModLtiIndexPage', {module: module, courseId: courseId}, options); + }, + buttons: [{ + icon: 'link', + label: 'addon.mod_lti.launchactivity', + action: (event: Event, navCtrl: NavController, module: any, courseId: number): void => { + const modal = this.domUtils.showModalLoading(); + + // Get LTI and launch data. + this.ltiProvider.getLti(courseId, module.id).then((ltiData) => { + return this.ltiProvider.getLtiLaunchData(ltiData.id).then((launchData) => { + // "View" LTI. + this.ltiProvider.logView(ltiData.id).then(() => { + this.courseProvider.checkModuleCompletion(courseId, module.completionstatus); + }).catch(() => { + // Ignore errors. + }); + + // Launch LTI. + return this.ltiProvider.launch(launchData.endpoint, launchData.parameters); + }); + }).catch((message) => { + this.domUtils.showErrorModalDefault(message, 'addon.mod_lti.errorgetlti', true); + }).finally(() => { + modal.dismiss(); + }); + } + }] + }; + + // Handle custom icons. + this.ltiProvider.getLti(courseId, module.id).then((ltiData) => { + const icon = ltiData.secureicon || ltiData.icon; + if (icon) { + const siteId = this.sitesProvider.getCurrentSiteId(); + this.filepoolProvider.downloadUrl(siteId, icon, false, AddonModLtiProvider.COMPONENT, module.id).then((url) => { + data.icon = url; + }).catch(() => { + // Error downloading. If we're online we'll set the online url. + if (this.appProvider.isOnline()) { + data.icon = icon; + } + }); + } + }).catch(() => { + // Ignore errors. + }); + + return data; + } + + /** + * Get the component to render the module. This is needed to support singleactivity course format. + * The component returned must implement CoreCourseModuleMainComponent. + * + * @param {any} course The course object. + * @param {any} module The module object. + * @return {any} The component to use, undefined if not found. + */ + getMainComponent(course: any, module: any): any { + return AddonModLtiIndexComponent; + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 37abd9b1d..7c565ae77 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -82,6 +82,7 @@ import { AddonModBookModule } from '@addon/mod/book/book.module'; import { AddonModChatModule } from '@addon/mod/chat/chat.module'; import { AddonModChoiceModule } from '@addon/mod/choice/choice.module'; import { AddonModLabelModule } from '@addon/mod/label/label.module'; +import { AddonModLtiModule } from '@addon/mod/lti/lti.module'; import { AddonModResourceModule } from '@addon/mod/resource/resource.module'; import { AddonModFeedbackModule } from '@addon/mod/feedback/feedback.module'; import { AddonModFolderModule } from '@addon/mod/folder/folder.module'; @@ -187,6 +188,7 @@ export const CORE_PROVIDERS: any[] = [ AddonModFeedbackModule, AddonModFolderModule, AddonModForumModule, + AddonModLtiModule, AddonModPageModule, AddonModQuizModule, AddonModScormModule,