MOBILE-2335 book: Implement provider and handlers

main
Dani Palou 2018-01-31 15:17:41 +01:00
parent 602d375a53
commit f8d71dd6ee
7 changed files with 543 additions and 2 deletions

View File

@ -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 { NgModule } from '@angular/core';
import { AddonModBookProvider } from './providers/book';
import { AddonModBookModuleHandler } from './providers/module-handler';
import { AddonModBookLinkHandler } from './providers/link-handler';
import { CoreCourseModuleDelegate } from '../../../core/course/providers/module-delegate';
import { CoreContentLinksDelegate } from '../../../core/contentlinks/providers/delegate';
@NgModule({
declarations: [
],
imports: [
],
providers: [
AddonModBookProvider,
AddonModBookModuleHandler,
AddonModBookLinkHandler
]
})
export class AddonModBookModule {
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModBookModuleHandler,
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModBookLinkHandler) {
moduleDelegate.registerHandler(moduleHandler);
contentLinksDelegate.registerHandler(linkHandler);
}
}

View File

@ -0,0 +1,3 @@
{
"errorchapter": "Error reading chapter of book."
}

View File

@ -0,0 +1,399 @@
// (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 { Http, Response } from '@angular/http';
import { CoreFileProvider } from '../../../../providers/file';
import { CoreFilepoolProvider } from '../../../../providers/filepool';
import { CoreLoggerProvider } from '../../../../providers/logger';
import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
import { CoreUtilsProvider } from '../../../../providers/utils/utils';
import { CoreCourseProvider } from '../../../../core/course/providers/course';
/**
* A book chapter inside the toc list.
*/
export interface AddonModBookTocChapter {
/**
* ID to identify the chapter.
* @type {string}
*/
id: string;
/**
* Chapter's title.
* @type {string}
*/
title: string;
/**
* The chapter's level.
* @type {number}
*/
level: number;
}
/**
* Map of book contents. For each chapter it has its index URL and the list of paths of the files the chapter has. Each path
* is identified by the relative path in the book, and the value is the URL of the file.
*/
export type AddonModBookContentsMap = {[chapter: string]: {indexUrl?: string, paths: {[path: string]: string}}};
/**
* Service that provides some features for books.
*/
@Injectable()
export class AddonModBookProvider {
static COMPONENT = 'mmaModBook';
protected ROOT_CACHE_KEY = 'mmaModBook:';
protected logger;
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider,
private fileProvider: CoreFileProvider, private filepoolProvider: CoreFilepoolProvider, private http: Http,
private utils: CoreUtilsProvider, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) {
this.logger = logger.getInstance('AddonModBookProvider');
}
/**
* Get a book by course module ID.
*
* @param {number} courseId Course ID.
* @param {number} cmId Course module ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the book is retrieved.
*/
getBook(courseId: number, cmId: number, siteId?: string): Promise<any> {
return this.getBookByField(courseId, 'coursemodule', cmId, siteId);
}
/**
* Get a book with key=value. If more than one is found, only the first will be returned.
*
* @param {number} courseId Course ID.
* @param {string} key Name of the property to check.
* @param {any} value Value to search.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the book is retrieved.
*/
protected getBookByField(courseId: number, key: string, value: any, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
courseids: [courseId]
},
preSets = {
cacheKey: this.getBookDataCacheKey(courseId)
};
return site.read('mod_book_get_books_by_courses', params, preSets).then((response) => {
// Search the book.
if (response && response.books) {
for (const i in response.books) {
const book = response.books[i];
if (book[key] == value) {
return book;
}
}
}
return Promise.reject(null);
});
});
}
/**
* Get cache key for get book data WS calls.
*
* @param {number} courseId Course ID.
* @return {string} Cache key.
*/
protected getBookDataCacheKey(courseId: number): string {
return this.ROOT_CACHE_KEY + 'book:' + courseId;
}
/**
* Gets a chapter contents.
*
* @param {AddonModBookContentsMap} contentsMap Contents map returned by getContentsMap.
* @param {string} chapterId Chapter to retrieve.
* @param {number} moduleId The module ID.
* @return {Promise<string>} Promise resolved with the contents.
*/
getChapterContent(contentsMap: AddonModBookContentsMap, chapterId: string, moduleId: number): Promise<string> {
const indexUrl = contentsMap[chapterId] ? contentsMap[chapterId].indexUrl : undefined,
siteId = this.sitesProvider.getCurrentSiteId();
let promise;
if (!indexUrl) {
// It shouldn't happen.
this.logger.debug('Could not locate the index chapter');
return Promise.reject(null);
}
if (this.fileProvider.isAvailable()) {
promise = this.filepoolProvider.downloadUrl(siteId, indexUrl, false, AddonModBookProvider.COMPONENT, moduleId);
} else {
// We return the live URL.
return Promise.resolve(this.sitesProvider.getCurrentSite().fixPluginfileURL(indexUrl));
}
return promise.then((url) => {
// Fetch the URL content.
const observable = this.http.get(url);
return this.utils.observableToPromise(observable).then((response: Response): any => {
const content = response.text();
if (typeof content !== 'string') {
return Promise.reject(null);
} else {
// Now that we have the content, we update the SRC to point back to the external resource.
return this.domUtils.restoreSourcesInHtml(content, contentsMap[chapterId].paths);
}
});
});
}
/**
* Convert an array of book contents into an object where contents are organized in chapters.
* Each chapter has an indexUrl and the list of contents in that chapter.
*
* @param {any[]} contents The module contents.
* @return {AddonModBookContentsMap} Contents map.
*/
getContentsMap(contents: any[]): AddonModBookContentsMap {
const map: AddonModBookContentsMap = {};
if (!contents) {
return map;
}
contents.forEach((content) => {
if (this.isFileDownloadable(content)) {
let chapter,
matches,
split,
filepathIsChapter,
key;
// Search the chapter number in the filepath.
matches = content.filepath.match(/\/(\d+)\//);
if (matches && matches[1]) {
chapter = matches[1];
filepathIsChapter = content.filepath == '/' + chapter + '/';
// Init the chapter if it's not defined yet.
map[chapter] = map[chapter] || { paths: {} };
if (content.filename == 'index.html' && filepathIsChapter) {
// Index of the chapter, set indexUrl of the chapter.
map[chapter].indexUrl = content.fileurl;
} else {
if (filepathIsChapter) {
// It's a file in the root folder OR the WS isn't returning the filepath as it should (MDL-53671).
// Try to get the path to the file from the URL.
split = content.fileurl.split('mod_book/chapter' + content.filepath);
key = split[1] || content.filename; // Use filename if we couldn't find the path.
} else {
// Remove the chapter folder from the path and add the filename.
key = content.filepath.replace('/' + chapter + '/', '') + content.filename;
}
map[chapter].paths[this.textUtils.decodeURIComponent(key)] = content.fileurl;
}
}
}
});
return map;
}
/**
* Get the first chapter of a book.
*
* @param {AddonModBookTocChapter[]} chapters The chapters list.
* @return {string} The chapter id.
*/
getFirstChapter(chapters: AddonModBookTocChapter[]): string {
if (!chapters || !chapters.length) {
return;
}
return chapters[0].id;
}
/**
* Get the next chapter to the given one.
*
* @param {AddonModBookTocChapter[]} chapters The chapters list.
* @param {string} chapterId The current chapter.
* @return {string} The next chapter id.
*/
getNextChapter(chapters: AddonModBookTocChapter[], chapterId: string): string {
let next = '0';
for (let i = 0; i < chapters.length; i++) {
if (chapters[i].id == chapterId) {
if (typeof chapters[i + 1] != 'undefined') {
next = chapters[i + 1].id;
break;
}
}
}
return next;
}
/**
* Get the previous chapter to the given one.
*
* @param {AddonModBookTocChapter[]} chapters The chapters list.
* @param {string} chapterId The current chapter.
* @return {string} The next chapter id.
*/
getPreviousChapter(chapters: AddonModBookTocChapter[], chapterId: string): string {
let previous = '0';
for (let i = 0; i < chapters.length; i++) {
if (chapters[i].id == chapterId) {
break;
}
previous = chapters[i].id;
}
return previous;
}
/**
* Get the book toc as an array.
*
* @param {any[]} contents The module contents.
* @return {any[]} The toc.
*/
getToc(contents: any[]): any[] {
if (!contents || !contents.length) {
return [];
}
return JSON.parse(contents[0].content);
}
/**
* Get the book toc as an array of chapters (not nested).
*
* @param {any[]} contents The module contents.
* @return {AddonModBookTocChapter[]} The toc as a list.
*/
getTocList(contents: any[]): AddonModBookTocChapter[] {
const chapters = [],
toc = this.getToc(contents);
toc.forEach((chapter) => {
// Add the chapter to the list.
let chapterId = chapter.href.replace('/index.html', '');
chapters.push({id: chapterId, title: chapter.title, level: chapter.level});
if (chapter.subitems) {
// Add all the subchapters to the list.
chapter.subitems.forEach((subChapter) => {
chapterId = subChapter.href.replace('/index.html', '');
chapters.push({id: chapterId, title: subChapter.title, level: subChapter.level});
});
}
});
return chapters;
}
/**
* Invalidates book data.
*
* @param {number} courseId Course ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateBookData(courseId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.invalidateWsCacheForKey(this.getBookDataCacheKey(courseId));
});
}
/**
* Invalidate the prefetched content.
*
* @param {number} moduleId The module ID.
* @param {number} courseId Course ID of the module.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const promises = [];
promises.push(this.invalidateBookData(courseId, siteId));
promises.push(this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModBookProvider.COMPONENT, moduleId));
promises.push(this.courseProvider.invalidateModule(moduleId, siteId));
return this.utils.allPromises(promises);
}
/**
* Check if a file is downloadable. The file param must have a 'type' attribute like in core_course_get_contents response.
*
* @param {any} file File to check.
* @return {boolean} Whether it's downloadable.
*/
isFileDownloadable(file: any): boolean {
return file.type === 'file';
}
/**
* Return whether or not the plugin is enabled.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
*/
isPluginEnabled(siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.canDownloadFiles();
});
}
/**
* Report a book as being viewed.
*
* @param {number} id Module ID.
* @param {string} chapterId Chapter ID.
* @return {Promise<any>} Promise resolved when the WS call is successful.
*/
logView(id: number, chapterId: string): Promise<any> {
if (id) {
const params = {
bookid: id,
chapterid: chapterId
};
return this.sitesProvider.getCurrentSite().write('mod_book_view_book', params).then((response) => {
if (!response.status) {
return Promise.reject(null);
}
});
}
return Promise.reject(null);
}
}

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 book.
*/
@Injectable()
export class AddonModBookLinkHandler extends CoreContentLinksModuleIndexHandler {
name = 'AddonModBookLinkHandler';
constructor(courseHelper: CoreCourseHelperProvider) {
super(courseHelper, 'mmaModBook', 'book');
}
}

View File

@ -0,0 +1,68 @@
// (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 { AddonModBookProvider } from './book';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '../../../../core/course/providers/module-delegate';
import { CoreCourseProvider } from '../../../../core/course/providers/course';
/**
* Handler to support book modules.
*/
@Injectable()
export class AddonModBookModuleHandler implements CoreCourseModuleHandler {
name = 'book';
constructor(protected bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider) { }
/**
* 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 this.bookProvider.isPluginEnabled();
}
/**
* 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 {
return {
icon: this.courseProvider.getModuleIconSrc('book'),
title: module.name,
class: 'addon-mod_book-handler',
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
// @todo
}
};
}
/**
* Get the component to render the module. This is needed to support singleactivity course format.
*
* @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 {
// @todo
}
}

View File

@ -66,6 +66,7 @@ import { CoreUserModule } from '../core/user/user.module';
import { AddonCalendarModule } from '../addon/calendar/calendar.module';
import { AddonUserProfileFieldModule } from '../addon/userprofilefield/userprofilefield.module';
import { AddonFilesModule } from '../addon/files/files.module';
import { AddonModBookModule } from '../addon/mod/book/book.module';
import { AddonModLabelModule } from '../addon/mod/label/label.module';
// For translate loader. AoT requires an exported function for factories.
@ -105,6 +106,7 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
AddonCalendarModule,
AddonUserProfileFieldModule,
AddonFilesModule,
AddonModBookModule,
AddonModLabelModule
],
bootstrap: [IonicApp],

View File

@ -21,6 +21,7 @@ import { CoreInitDelegate } from '../../../providers/init';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
import { CoreTextUtilsProvider } from '../../../providers/utils/text';
import { CoreUrlUtilsProvider } from '../../../providers/utils/url';
import { CoreLoginHelperProvider } from '../../login/providers/helper';
import { CoreContentLinksDelegate, CoreContentLinksAction } from './delegate';
@ -37,7 +38,7 @@ export class CoreContentLinksHelperProvider {
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private loginHelper: CoreLoginHelperProvider,
private contentLinksDelegate: CoreContentLinksDelegate, private appProvider: CoreAppProvider,
private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private translate: TranslateService,
private initDelegate: CoreInitDelegate, eventsProvider: CoreEventsProvider) {
private initDelegate: CoreInitDelegate, eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider) {
this.logger = logger.getInstance('CoreContentLinksHelperProvider');
// Listen for app launched URLs. If we receive one, check if it's a content link.
@ -103,7 +104,7 @@ export class CoreContentLinksHelperProvider {
const modal = this.domUtils.showModalLoading();
let username;
url = decodeURIComponent(url);
url = this.textUtils.decodeURIComponent(url);
// App opened using custom URL scheme.
this.logger.debug('Treating custom URL scheme: ' + url);