MOBILE-2333 course: Implement download module main file directive

main
Dani Palou 2018-02-19 09:26:56 +01:00
parent adbee2991c
commit fec9fa6efa
10 changed files with 429 additions and 8 deletions

View File

@ -28,6 +28,7 @@ import { CoreComponentsModule } from '../components.module';
import { CoreDirectivesModule } from '../../directives/directives.module';
import { CorePipesModule } from '../../pipes/pipes.module';
import { CoreCourseComponentsModule } from '../../core/course/components/components.module';
import { CoreCourseDirectivesModule } from '../../core/course/directives/directives.module';
import { CoreCoursesComponentsModule } from '../../core/courses/components/components.module';
import { CoreSiteHomeComponentsModule } from '../../core/sitehome/components/components.module';
import { CoreUserComponentsModule } from '../../core/user/components/components.module';
@ -80,7 +81,8 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy {
// List of imports for dynamic module. Since the template can have any component we need to import all core components modules.
protected IMPORTS = [
IonicModule, TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, CorePipesModule,
CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreSiteHomeComponentsModule, CoreUserComponentsModule
CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreSiteHomeComponentsModule, CoreUserComponentsModule,
CoreCourseDirectivesModule
];
// Other Ionic/Angular providers that don't depend on where they are injected.

View File

@ -0,0 +1,27 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CoreCourseDownloadModuleMainFileDirective } from './download-module-main-file';
@NgModule({
declarations: [
CoreCourseDownloadModuleMainFileDirective
],
imports: [],
exports: [
CoreCourseDownloadModuleMainFileDirective
]
})
export class CoreCourseDirectivesModule {}

View File

@ -0,0 +1,83 @@
// (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 { Directive, Input, OnInit, ElementRef } from '@angular/core';
import { NavController } from 'ionic-angular';
import { CoreCourseProvider } from '../providers/course';
import { CoreCourseHelperProvider } from '../providers/helper';
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
import { CoreUtilsProvider } from '../../../providers/utils/utils';
/**
* Directive to allow downloading and open the main file of a module.
* When the item with this directive is clicked, the module will be downloaded (if needed) and opened.
* This is meant for modules like mod_resource.
*
* This directive must receive either a module or a moduleId. If no files are provided, it will use module.contents.
*/
@Directive({
selector: '[core-course-download-module-main-file]'
})
export class CoreCourseDownloadModuleMainFileDirective implements OnInit {
@Input() module: any; // The module.
@Input() moduleId: string | number; // The module ID. Required if module is not supplied.
@Input() courseId: string | number; // The course ID.
@Input() component?: string; // Component to link the file to.
@Input() componentId?: string | number; // Component ID to use in conjunction with the component. If not defined, use moduleId.
@Input() files?: any[]; // List of files of the module. If not provided, use module.contents.
protected element: HTMLElement;
constructor(element: ElementRef, protected domUtils: CoreDomUtilsProvider, protected courseHelper: CoreCourseHelperProvider,
protected courseProvider: CoreCourseProvider) {
this.element = element.nativeElement || element;
}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.element.addEventListener('click', (ev: Event): void => {
if (!this.module && !this.moduleId) {
return;
}
ev.preventDefault();
ev.stopPropagation();
const modal = this.domUtils.showModalLoading(),
courseId = typeof this.courseId == 'string' ? parseInt(this.courseId, 10) : this.courseId;
let promise;
if (this.module) {
// We already have the module.
promise = Promise.resolve(module);
} else {
// Try to get the module from cache.
this.moduleId = typeof this.moduleId == 'string' ? parseInt(this.moduleId, 10) : this.moduleId;
promise = this.courseProvider.getModule(this.moduleId, courseId);
}
promise.then((module) => {
const componentId = this.componentId || module.id;
return this.courseHelper.downloadModuleAndOpenFile(module, courseId, this.component, componentId, this.files);
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
}).finally(() => {
modal.dismiss();
});
});
}
}

View File

@ -201,7 +201,7 @@ export class CoreCourseProvider {
* @return {Promise<any>} Promise resolved with the module.
*/
getModule(moduleId: number, courseId?: number, sectionId?: number, preferCache?: boolean, ignoreCache?: boolean,
siteId?: string): Promise<any> {
siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
let promise;

View File

@ -15,8 +15,11 @@
import { Injectable } from '@angular/core';
import { NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events';
import { CoreFileProvider } from '@providers/file';
import { CoreFilepoolProvider } from '@providers/filepool';
import { CoreFileHelperProvider } from '@providers/file-helper';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
@ -116,7 +119,8 @@ export class CoreCourseHelperProvider {
private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider,
private utils: CoreUtilsProvider, private translate: TranslateService, private loginHelper: CoreLoginHelperProvider,
private courseOptionsDelegate: CoreCourseOptionsDelegate, private siteHomeProvider: CoreSiteHomeProvider,
private eventsProvider: CoreEventsProvider) { }
private eventsProvider: CoreEventsProvider, private fileHelper: CoreFileHelperProvider,
private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider) { }
/**
* This function treats every module on the sections provided to load the handler data, treat completion
@ -470,6 +474,227 @@ export class CoreCourseHelperProvider {
});
}
/**
* Convenience function to open a module main file, downloading the package if needed.
* This is meant for modules like mod_resource.
*
* @param {any} module The module to download.
* @param {number} courseId The course ID of the module.
* @param {string} [component] The component to link the files to.
* @param {string|number} [componentId] An ID to use in conjunction with the component.
* @param {any[]} [files] List of files of the module. If not provided, use module.contents.
* @param {string} [siteId] The site ID. If not defined, current site.
* @return {Promise<any>} Resolved on success.
*/
downloadModuleAndOpenFile(module: any, courseId: number, component?: string, componentId?: string | number, files?: any[],
siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
let promise;
if (files) {
promise = Promise.resolve(files);
} else {
promise = this.courseProvider.loadModuleContents(module, courseId).then(() => {
files = module.contents;
});
}
// Make sure that module contents are loaded.
return promise.then(() => {
if (!files || !files.length) {
return Promise.reject(null);
}
return this.sitesProvider.getSite(siteId);
}).then((site) => {
const mainFile = files[0],
fileUrl = this.fileHelper.getFileUrl(mainFile);
// Check if the file should be opened in browser.
if (this.fileHelper.shouldOpenInBrowser(mainFile)) {
if (this.appProvider.isOnline()) {
// Open in browser.
let fixedUrl = site.fixPluginfileURL(fileUrl).replace('&offline=1', '');
// Remove forcedownload when followed by another param.
fixedUrl = fixedUrl.replace(/forcedownload=\d+&/, '');
// Remove forcedownload when not followed by any param.
fixedUrl = fixedUrl.replace(/[\?|\&]forcedownload=\d+/, '');
this.utils.openInBrowser(fixedUrl);
if (this.fileProvider.isAvailable()) {
// Download the file if needed (file outdated or not downloaded).
// Download will be in background, don't return the promise.
this.downloadModule(module, courseId, component, componentId, files, siteId);
}
return;
} else {
// Not online, get the offline file. It will fail if not found.
return this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl).then((path) => {
return this.utils.openFile(path);
}).catch((error) => {
return Promise.reject(this.translate.instant('core.networkerrormsg'));
});
}
}
// File shouldn't be opened in browser. Download the module if it needs to be downloaded.
return this.downloadModuleWithMainFileIfNeeded(module, courseId, component, componentId, files, siteId)
.then((result) => {
if (result.path.indexOf('http') === 0) {
return this.utils.openOnlineFile(result.path).catch((error) => {
// Error opening the file, some apps don't allow opening online files.
if (!this.fileProvider.isAvailable()) {
return Promise.reject(error);
} else if (result.status === CoreConstants.DOWNLOADING) {
return Promise.reject(this.translate.instant('core.erroropenfiledownloading'));
}
let promise;
if (result.status === CoreConstants.NOT_DOWNLOADED) {
// Not downloaded, download it now and return the local file.
promise = this.downloadModule(module, courseId, component, componentId, files, siteId).then(() => {
return this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl);
});
} else {
// File is outdated or stale and can't be opened in online, return the local URL.
promise = this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl);
}
return promise.then((path) => {
return this.utils.openFile(path);
});
});
} else {
return this.utils.openFile(result.path);
}
});
});
}
/**
* Convenience function to download a module that has a main file and return the local file's path and other info.
* This is meant for modules like mod_resource.
*
* @param {any} module The module to download.
* @param {number} courseId The course ID of the module.
* @param {string} [component] The component to link the files to.
* @param {string|number} [componentId] An ID to use in conjunction with the component.
* @param {any[]} [files] List of files of the module. If not provided, use module.contents.
* @param {string} [siteId] The site ID. If not defined, current site.
* @return {Promise<{fixedUrl: string, path: string, status: string}>} Promise resolved when done.
*/
protected downloadModuleWithMainFileIfNeeded(module: any, courseId: number, component?: string, componentId?: string | number,
files?: any[], siteId?: string): Promise<{fixedUrl: string, path: string, status: string}> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (!files || !files.length) {
// Module not valid, stop.
return Promise.reject(null);
}
const mainFile = files[0],
fileUrl = this.fileHelper.getFileUrl(mainFile),
timemodified = this.fileHelper.getFileTimemodified(mainFile),
prefetchHandler = this.prefetchDelegate.getPrefetchHandlerFor(module),
result = {
fixedUrl: undefined,
path: undefined,
status: undefined
};
return this.sitesProvider.getSite(siteId).then((site) => {
const fixedUrl = site.fixPluginfileURL(fileUrl);
result.fixedUrl = fixedUrl;
if (this.fileProvider.isAvailable()) {
// The file system is available.
return this.filepoolProvider.getPackageStatus(siteId, component, componentId).then((status) => {
result.status = status;
const isWifi = !this.appProvider.isNetworkAccessLimited(),
isOnline = this.appProvider.isOnline();
if (status === CoreConstants.DOWNLOADED) {
// Get the local file URL.
return this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl);
} else if (status === CoreConstants.DOWNLOADING && !this.appProvider.isDesktop()) {
// Return the online URL.
return fixedUrl;
} else {
if (!isOnline && status === CoreConstants.NOT_DOWNLOADED) {
// Not downloaded and we're offline, reject.
return Promise.reject(this.translate.instant('core.networkerrormsg'));
}
return this.filepoolProvider.shouldDownloadBeforeOpen(fixedUrl, mainFile.filesize).then(() => {
// Download and then return the local URL.
return this.downloadModule(module, courseId, component, componentId, files, siteId).then(() => {
return this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl);
});
}, () => {
// Start the download if in wifi, but return the URL right away so the file is opened.
if (isWifi && isOnline) {
this.downloadModule(module, courseId, component, componentId, files, siteId);
}
if (!this.fileHelper.isStateDownloaded(status) || isOnline) {
// Not downloaded or online, return the online URL.
return fixedUrl;
} else {
// Outdated but offline, so we return the local URL. Use getUrlByUrl so it's added to the queue.
return this.filepoolProvider.getUrlByUrl(siteId, fileUrl, component, componentId, timemodified,
false, false, mainFile);
}
});
}
}).then((path) => {
result.path = path;
return result;
});
} else {
// We use the live URL.
result.path = fixedUrl;
return result;
}
});
}
/**
* Convenience function to download a module.
*
* @param {any} module The module to download.
* @param {number} courseId The course ID of the module.
* @param {string} [component] The component to link the files to.
* @param {string|number} [componentId] An ID to use in conjunction with the component.
* @param {any[]} [files] List of files of the module. If not provided, use module.contents.
* @param {string} [siteId] The site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when done.
*/
downloadModule(module: any, courseId: number, component?: string, componentId?: string | number, files?: any[], siteId?: string)
: Promise<any> {
const prefetchHandler = this.prefetchDelegate.getPrefetchHandlerFor(module);
if (prefetchHandler) {
// Use the prefetch handler to download the module.
if (prefetchHandler.download) {
return prefetchHandler.download(module, courseId);
} else {
return prefetchHandler.prefetch(module, courseId, true);
}
}
// There's no prefetch handler for the module, just download the files.
files = files || module.contents;
return this.filepoolProvider.downloadOrPrefetchFiles(siteId, files, false, false, component, componentId);
}
/**
* Fill the Context Menu for a certain module.
*

View File

@ -86,9 +86,20 @@ export interface CoreCourseModulePrefetchHandler extends CoreDelegateHandler {
* @param {any} module Module.
* @param {number} courseId Course ID the module belongs to.
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
* @param {string} [dirPath] Path of the directory where to store all the content files.
* @return {Promise<any>} Promise resolved when done.
*/
prefetch(module: any, courseId?: number, single?: boolean): Promise<any>;
prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any>;
/**
* Download the module.
*
* @param {any} module The module object returned by WS.
* @param {number} courseId Course ID.
* @param {string} [dirPath] Path of the directory where to store all the content files.
* @return {Promise<any>} Promise resolved when all content is downloaded.
*/
download?(module: any, courseId: number, dirPath?: string): Promise<any>;
/**
* Check if a certain module can use core_course_check_updates to check if it has updates.

View File

@ -109,7 +109,17 @@ export class CoreSiteAddonsModulePrefetchHandler extends CoreCourseModulePrefetc
promises.push(this.siteAddonsProvider.callWS(method, params, {cacheKey: cacheKey}));
} else {
// It's a method to get content.
promises.push(this.siteAddonsProvider.getContent(this.component, method, args));
promises.push(this.siteAddonsProvider.getContent(this.component, method, args).then((result) => {
const subPromises = [];
// Prefetch the files in the content.
if (result.files && result.files.length) {
subPromises.push(this.filepoolProvider.downloadOrPrefetchFiles(siteId, result.files, prefetch, false,
this.component, module.id, dirPath));
}
return Promise.all(subPromises);
}));
}
}

View File

@ -43,6 +43,32 @@ export interface CoreSiteAddonsModuleHandler {
handlerSchema: any;
}
export interface CoreSiteAddonsGetContentResult {
/**
* The content in HTML.
* @type {string}
*/
html: string;
/**
* The javascript for the content.
* @type {string}
*/
javascript: string;
/**
* The files for the content.
* @type {any[]}
*/
files?: any[];
/**
* Other data.
* @type {any}
*/
otherdata?: any;
}
/**
* Service to provide functionalities regarding site addons.
*/
@ -104,9 +130,9 @@ export class CoreSiteAddonsProvider {
* @param {string} method Method to execute in the class.
* @param {any} args The params for the method.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<{html: string, javascript: string}>} Promise resolved with the content and the javascript.
* @return {Promise<CoreSiteAddonsGetContentResult>} Promise resolved with the result.
*/
getContent(component: string, method: string, args: any, siteId?: string): Promise<{html: string, javascript: string}> {
getContent(component: string, method: string, args: any, siteId?: string): Promise<CoreSiteAddonsGetContentResult> {
this.logger.debug(`Get content for component '${component}' and method '${method}'`);
return this.sitesProvider.getSite(siteId).then((site) => {
@ -129,6 +155,16 @@ export class CoreSiteAddonsProvider {
};
return this.sitesProvider.getCurrentSite().read('tool_mobile_get_content', data, preSets);
}).then((result) => {
if (result.otherdata) {
try {
result.otherdata = JSON.parse(result.otherdata);
} catch (ex) {
// Ignore errors.
}
}
return result;
});
});
}

View File

@ -237,4 +237,31 @@ export class CoreFileHelperProvider {
isStateDownloaded(state: string): boolean {
return state === CoreConstants.DOWNLOADED || state === CoreConstants.OUTDATED;
}
/**
* Whether the file has to be opened in browser (external repository).
* The file must have a mimetype attribute.
*
* @param {any} file The file to check.
* @return {boolean} Whether the file should be opened in browser.
*/
shouldOpenInBrowser(file: any): boolean {
if (!file || !file.isexternalfile || !file.mimetype) {
return false;
}
const mimetype = file.mimetype;
if (mimetype.indexOf('application/vnd.google-apps.') != -1) {
// Google Docs file, always open in browser.
return true;
}
if (file.repositorytype == 'onedrive') {
// In OneDrive, open in browser the office docs
return mimetype.indexOf('application/vnd.openxmlformats-officedocument') != -1 ||
mimetype == 'text/plain' || mimetype == 'document/unknown';
}
return false;
}
}

View File

@ -2718,7 +2718,7 @@ export class CoreFilepoolProvider {
*/
storePackageStatus(siteId: string, status: string, component: string, componentId?: string | number, extra?: string)
: Promise<any> {
this.logger.debug(`Set status '${status}'' for package ${component} ${componentId}`);
this.logger.debug(`Set status '${status}' for package ${component} ${componentId}`);
componentId = this.fixComponentId(componentId);
return this.sitesProvider.getSite(siteId).then((site) => {