Merge pull request #1312 from albertgasset/MOBILE-2346

Mobile 2346
main
Juan Leyva 2018-05-17 12:28:29 +02:00 committed by GitHub
commit 981e1a6aac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 693 additions and 0 deletions

View File

@ -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 {}

View File

@ -0,0 +1,21 @@
<!-- Buttons to add to the header. -->
<core-navbar-buttons end>
<core-context-menu>
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item>
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item>
<core-context-menu-item *ngIf="loaded && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center">
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
<div padding-horizontal>
<button ion-button block icon-left (click)="launch()">
<ion-icon name="link"></ion-icon>
{{ 'addon.mod_lti.launchactivity' | translate }}
</button>
</div>
</core-loading>

View File

@ -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 { Component, Optional, Injector } from '@angular/core';
import { Content } from 'ionic-angular';
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.
protected fetchContentDefaultError = 'addon.mod_lti.errorgetlti';
constructor(injector: Injector,
@Optional() protected content: Content,
private ltiProvider: AddonModLtiProvider) {
super(injector, content);
}
/**
* Component being initialized.
*/
ngOnInit(): void {
super.ngOnInit();
this.loadContent();
}
/**
* 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<any>} Promise resolved when done.
*/
protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> {
return this.ltiProvider.getLti(this.courseId, this.module.id).then((ltiData) => {
this.lti = ltiData;
this.description = this.lti.intro || this.description;
this.dataRetrieved.emit(this.lti);
}).then(() => {
// All data obtained, now fill the context menu.
this.fillContextMenu(refresh);
});
}
/**
* Perform the invalidate content function.
*
* @return {Promise<any>} Resolved when done.
*/
protected invalidateContent(): Promise<any> {
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 {
this.ltiProvider.getLtiLaunchData(this.lti.id).then((launchData) => {
// "View" LTI.
this.ltiProvider.logView(this.lti.id).then(() => {
this.checkCompletion();
}).catch((error) => {
// Ignore errors.
});
// Launch LTI.
return this.ltiProvider.launch(launchData.endpoint, launchData.parameters);
}).catch((message) => {
this.domUtils.showErrorModalDefault(message, 'core.error', true);
});
}
}

View File

@ -0,0 +1,5 @@
{
"errorgetlti": "Error getting module data.",
"errorinvalidlaunchurl": "The launch URL is not valid.",
"launchactivity": "Launch the activity"
}

View File

@ -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);
}
}

View File

@ -0,0 +1,16 @@
<ion-header>
<ion-navbar>
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
<ion-buttons end>
<!-- The buttons defined by the component will be added in here. -->
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="ltiComponent.loaded" (ionRefresh)="ltiComponent.doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<addon-mod-lti-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-lti-index>
</ion-content>

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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');
}
}

View File

@ -0,0 +1,217 @@
// (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<any>} Promise resolved when the launcher file is deleted.
*/
deleteLauncher(): Promise<any> {
return this.fileProvider.removeFile(this.LAUNCHER_FILE_NAME);
}
/**
* Generates a launcher file.
*
* @param {string} url Launch URL.
* @param {AddonModLtiParam[]} params Launch params.
* @return {Promise<string>} Promise resolved with the file URL.
*/
generateLauncher(url: string, params: AddonModLtiParam[]): Promise<string> {
if (!this.fileProvider.isAvailable()) {
return Promise.resolve(url);
}
// Generate a form with the params.
let text = '<form action="' + url + '" name="ltiLaunchForm" ' +
'method="post" encType="application/x-www-form-urlencoded">\n';
params.forEach((p) => {
if (p.name == 'ext_submit') {
text += ' <input type="submit"';
} else {
text += ' <input type="hidden" name="' + this.textUtils.escapeHTML(p.name) + '"';
}
text += ' value="' + this.textUtils.escapeHTML(p.value) + '"/>\n';
});
text += '</form>\n';
// Add an in-line script to automatically submit the form.
text += '<script type="text/javascript"> \n' +
' window.onload = function() { \n' +
' document.ltiLaunchForm.submit(); \n' +
' }; \n' +
'</script> \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<any>} Promise resolved when the LTI is retrieved.
*/
getLti(courseId: number, cmId: number): Promise<any> {
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<any>} Promise resolved when the launch data is retrieved.
*/
getLtiLaunchData(id: number): Promise<any> {
const params: any = {
toolid: id
};
// Try to avoid using cache since the "nonce" parameter is set to a timestamp.
const preSets = {
getFromCache: false,
saveToCache: true,
emergencyCache: true,
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<any>} Promise resolved when the data is invalidated.
*/
invalidateLti(courseId: number): Promise<any> {
return this.sitesProvider.getCurrentSite().invalidateWsCacheForKey(this.getLtiCacheKey(courseId));
}
/**
* Invalidates options.
*
* @param {number} id LTI id.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateLtiLaunchData(id: number): Promise<any> {
return this.sitesProvider.getCurrentSite().invalidateWsCacheForKey(this.getLtiLaunchDataCacheKey(id));
}
/**
* Launch LTI.
*
* @param {string} url Launch URL.
* @param {AddonModLtiParam[]} params Launch params.
* @return {Promise<any>} Promise resolved when the WS call is successful.
*/
launch(url: string, params: AddonModLtiParam[]): Promise<any> {
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);
});
}
/**
* Report the LTI as being viewed.
*
* @param {string} id LTI id.
* @return {Promise<any>} Promise resolved when the WS call is successful.
*/
logView(id: string): Promise<any> {
const params: any = {
ltiid: id
};
return this.sitesProvider.getCurrentSite().write('mod_lti_view_lti', params);
}
}

View File

@ -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<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
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;
}
}

View File

@ -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,