MOBILE-2345 lesson: Implement sync provider and prefetch handler

main
Dani Palou 2018-04-30 12:16:28 +02:00
parent 9abc6748ae
commit 92182ed720
6 changed files with 1048 additions and 1 deletions

View File

@ -0,0 +1,26 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'core.login.password' | translate }}</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="close"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content padding class="addon-mod_lesson-password-modal">
<form ion-list (ngSubmit)="submitPassword(passwordinput)">
<ion-item>
<core-show-password item-content [name]="'password'">
<ion-label>{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label>
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" core-auto-focus #passwordinput></ion-input>
</core-show-password>
</ion-item>
<ion-item>
<button ion-button block type="submit" icon-end>
{{ 'addon.mod_lesson.continue' | translate }}
<ion-icon name="arrow-forward" md="ios-arrow-forward"></ion-icon>
</button>
</ion-item>
</form>
</ion-content>

View File

@ -0,0 +1,31 @@
// (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 { AddonModLessonPasswordModalPage } from './password-modal';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
@NgModule({
declarations: [
AddonModLessonPasswordModalPage
],
imports: [
CoreComponentsModule,
IonicPageModule.forChild(AddonModLessonPasswordModalPage),
TranslateModule.forChild()
]
})
export class AddonModLessonPasswordModalPageModule {}

View File

@ -0,0 +1,43 @@
// (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 } from '@angular/core';
import { IonicPage, ViewController } from 'ionic-angular';
/**
* Modal that asks the password for a lesson.
*/
@IonicPage({ segment: 'addon-mod-lesson-password-modal' })
@Component({
selector: 'page-addon-mod-lesson-password-modal',
templateUrl: 'password-modal.html',
})
export class AddonModLessonPasswordModalPage {
constructor(protected viewCtrl: ViewController) { }
/**
* Send the password back.
*/
submitPassword(password: HTMLInputElement): void {
this.viewCtrl.dismiss(password.value);
}
/**
* Close modal.
*/
closeModal(): void {
this.viewCtrl.dismiss();
}
}

View File

@ -0,0 +1,498 @@
// (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 { CoreAppProvider } from '@providers/app';
import { CoreEventsProvider } from '@providers/events';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
import { CoreSyncProvider } from '@providers/sync';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreSyncBaseProvider } from '@classes/base-sync';
import { AddonModLessonProvider } from './lesson';
import { AddonModLessonOfflineProvider } from './lesson-offline';
import { AddonModLessonPrefetchHandler } from './prefetch-handler';
/**
* Data returned by a lesson sync.
*/
export interface AddonModLessonSyncResult {
/**
* List of warnings.
* @type {string[]}
*/
warnings: string[];
/**
* Whether some data was sent to the server or offline data was updated.
* @type {boolean}
*/
updated: boolean;
}
/**
* Service to sync lesson.
*/
@Injectable()
export class AddonModLessonSyncProvider extends CoreSyncBaseProvider {
static AUTO_SYNCED = 'addon_mod_lesson_autom_synced';
static SYNC_TIME = 300000;
protected componentTranslate: string;
// Variables for database.
protected RETAKES_FINISHED_TABLE = 'addon_mod_lesson_retakes_finished_sync';
protected tablesSchema = {
name: this.RETAKES_FINISHED_TABLE,
columns: [
{
name: 'lessonId',
type: 'INTEGER',
primaryKey: true
},
{
name: 'retake',
type: 'INTEGER'
},
{
name: 'pageId',
type: 'INTEGER'
},
{
name: 'timefinished',
type: 'INTEGER'
}
]
};
constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider,
syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService,
courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider,
private lessonProvider: AddonModLessonProvider, private lessonOfflineProvider: AddonModLessonOfflineProvider,
private prefetchHandler: AddonModLessonPrefetchHandler, private timeUtils: CoreTimeUtilsProvider,
private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider) {
super('AddonModLessonSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate);
this.componentTranslate = courseProvider.translateModuleName('lesson');
this.sitesProvider.createTableFromSchema(this.tablesSchema);
}
/**
* Unmark a retake as finished in a synchronization.
*
* @param {number} lessonId Lesson ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when done.
*/
deleteRetakeFinishedInSync(lessonId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().deleteRecords(this.RETAKES_FINISHED_TABLE, {lessonId});
}).catch(() => {
// Ignore errors, maybe there is none.
});
}
/**
* Get a retake finished in a synchronization for a certain lesson (if any).
*
* @param {number} lessonId Lesson ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with the retake entry (undefined if no retake).
*/
getRetakeFinishedInSync(lessonId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecord(this.RETAKES_FINISHED_TABLE, {lessonId});
}).catch(() => {
// Ignore errors, return undefined.
});
}
/**
* Check if a lesson has data to synchronize.
*
* @param {number} lessonId Lesson ID.
* @param {number} retake Retake number.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} Promise resolved with boolean: whether it has data to sync.
*/
hasDataToSync(lessonId: number, retake: number, siteId?: string): Promise<boolean> {
const promises = [];
let hasDataToSync = false;
promises.push(this.lessonOfflineProvider.hasRetakeAttempts(lessonId, retake, siteId).then((hasAttempts) => {
hasDataToSync = hasDataToSync || hasAttempts;
}).catch(() => {
// Ignore errors.
}));
promises.push(this.lessonOfflineProvider.hasFinishedRetake(lessonId, siteId).then((hasFinished) => {
hasDataToSync = hasDataToSync || hasFinished;
}));
return Promise.all(promises).then(() => {
return hasDataToSync;
});
}
/**
* Mark a retake as finished in a synchronization.
*
* @param {number} lessonId Lesson ID.
* @param {number} retake The retake number.
* @param {number} pageId The page ID to start reviewing from.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when done.
*/
setRetakeFinishedInSync(lessonId: number, retake: number, pageId: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().insertRecord(this.RETAKES_FINISHED_TABLE, {
lessonId: lessonId,
retake: Number(retake),
pageId: Number(pageId),
timefinished: this.timeUtils.timestamp()
});
});
}
/**
* Try to synchronize all the lessons in a certain site or in all sites.
*
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
*/
syncAllLessons(siteId?: string): Promise<any> {
return this.syncOnSites('all lessons', this.syncAllLessonsFunc.bind(this), [], siteId);
}
/**
* Sync all lessons on a site.
*
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
* @param {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
*/
protected syncAllLessonsFunc(siteId?: string): Promise<any> {
// Get all the lessons that have something to be synchronized.
return this.lessonOfflineProvider.getAllLessonsWithData(siteId).then((lessons) => {
// Sync all lessons that haven't been synced for a while.
const promises = [];
lessons.forEach((lesson) => {
promises.push(this.syncLessonIfNeeded(lesson.id, false, siteId).then((result) => {
if (result && result.updated) {
// Sync successful, send event.
this.eventsProvider.trigger(AddonModLessonSyncProvider.AUTO_SYNCED, {
lessonId: lesson.id,
warnings: result.warnings
}, siteId);
}
}));
});
return Promise.all(promises);
});
}
/**
* Sync a lesson only if a certain time has passed since the last time.
*
* @param {any} lessonId Lesson ID.
* @param {boolean} [askPreflight] Whether we should ask for password if needed.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when the lesson is synced or if it doesn't need to be synced.
*/
syncLessonIfNeeded(lessonId: number, askPassword?: boolean, siteId?: string): Promise<any> {
return this.isSyncNeeded(lessonId, siteId).then((needed) => {
if (needed) {
return this.syncLesson(lessonId, askPassword, false, siteId);
}
});
}
/**
* Try to synchronize a lesson.
*
* @param {number} lessonId Lesson ID.
* @param {boolean} askPassword True if we should ask for password if needed, false otherwise.
* @param {boolean} ignoreBlock True to ignore the sync block setting.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<AddonModLessonSyncResult>} Promise resolved in success.
*/
syncLesson(lessonId: number, askPassword?: boolean, ignoreBlock?: boolean, siteId?: string): Promise<AddonModLessonSyncResult> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const result: AddonModLessonSyncResult = {
warnings: [],
updated: false
};
let syncPromise,
lesson,
courseId,
password,
accessInfo;
if (this.isSyncing(lessonId, siteId)) {
// There's already a sync ongoing for this lesson, return the promise.
return this.getOngoingSync(lessonId, siteId);
}
// Verify that lesson isn't blocked.
if (!ignoreBlock && this.syncProvider.isBlocked(AddonModLessonProvider.COMPONENT, lessonId, siteId)) {
this.logger.debug('Cannot sync lesson ' + lessonId + ' because it is blocked.');
return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate}));
}
this.logger.debug('Try to sync lesson ' + lessonId + ' in site ' + siteId);
// Try to synchronize the attempts first.
syncPromise = this.lessonOfflineProvider.getLessonAttempts(lessonId, siteId).then((attempts) => {
if (!attempts.length) {
return;
} else if (!this.appProvider.isOnline()) {
// Cannot sync in offline.
return Promise.reject(null);
}
courseId = attempts[0].courseid;
// Get the info, access info and the lesson password if needed.
return this.lessonProvider.getLessonById(courseId, lessonId, false, siteId).then((lessonData) => {
lesson = lessonData;
return this.prefetchHandler.getLessonPassword(lessonId, false, true, askPassword, siteId);
}).then((data) => {
const attemptsLength = attempts.length,
promises = [];
accessInfo = data.accessInfo;
password = data.password;
lesson = data.lesson || lesson;
// Filter the attempts, get only the ones that belong to the current retake.
attempts = attempts.filter((attempt) => {
if (attempt.retake != accessInfo.attemptscount) {
// Attempt doesn't belong to current retake, delete.
promises.push(this.lessonOfflineProvider.deleteAttempt(lesson.id, attempt.retake, attempt.pageid,
attempt.timemodified, siteId).catch(() => {
// Ignore errors.
}));
return false;
}
return true;
});
if (attempts.length != attemptsLength) {
// Some attempts won't be sent, add a warning.
result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
component: this.componentTranslate,
name: lesson.name,
error: this.translate.instant('addon.mod_lesson.warningretakefinished')
}));
}
return Promise.all(promises);
}).then(() => {
if (!attempts.length) {
return;
}
// Send the attempts in the same order they were answered.
attempts.sort((a, b) => {
return a.timemodified - b.timemodified;
});
attempts = attempts.map((attempt) => {
return {
func: this.sendAttempt.bind(this),
params: [lesson, password, attempt, result, siteId],
blocking: true
};
});
return this.utils.executeOrderedPromises(attempts);
});
}).then(() => {
// Attempts sent or there was none. If there is a finished retake, send it.
return this.lessonOfflineProvider.getRetake(lessonId, siteId).then((retake) => {
if (!retake.finished) {
// The retake isn't marked as finished, nothing to send. Delete the retake.
return this.lessonOfflineProvider.deleteRetake(lessonId, siteId);
} else if (!this.appProvider.isOnline()) {
// Cannot sync in offline.
return Promise.reject(null);
}
let promise;
courseId = retake.courseid || courseId;
if (lesson) {
// Data already retrieved when syncing attempts.
promise = Promise.resolve();
} else {
promise = this.lessonProvider.getLessonById(courseId, lessonId, false, siteId).then((lessonData) => {
lesson = lessonData;
return this.prefetchHandler.getLessonPassword(lessonId, false, true, askPassword, siteId);
}).then((data) => {
accessInfo = data.accessInfo;
password = data.password;
lesson = data.lesson || lesson;
});
}
return promise.then(() => {
if (retake.retake != accessInfo.attemptscount) {
// The retake changed, add a warning if it isn't there already.
if (!result.warnings.length) {
result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
component: this.componentTranslate,
name: lesson.name,
error: this.translate.instant('addon.mod_lesson.warningretakefinished')
}));
}
return this.lessonOfflineProvider.deleteRetake(lessonId, siteId);
}
// All good, finish the retake.
return this.lessonProvider.finishRetakeOnline(lessonId, password, false, false, siteId).then((response) => {
result.updated = true;
if (!ignoreBlock) {
// Mark the retake as finished in a sync if it can be reviewed.
if (response.data && response.data.reviewlesson) {
const params = this.urlUtils.extractUrlParams(response.data.reviewlesson.value);
if (params && params.pageid) {
// The retake can be reviewed, mark it as finished. Don't block the user for this.
this.setRetakeFinishedInSync(lessonId, retake.retake, params.pageid, siteId);
}
}
}
return this.lessonOfflineProvider.deleteRetake(lessonId, siteId);
}).catch((error) => {
if (error && this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
result.updated = true;
return this.lessonOfflineProvider.deleteRetake(lessonId, siteId).then(() => {
// Retake deleted, add a warning.
result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
component: this.componentTranslate,
name: lesson.name,
error: error
}));
});
} else {
// Couldn't connect to server, reject.
return Promise.reject(error);
}
});
});
}, () => {
// No retake stored, nothing to do.
});
}).then(() => {
if (result.updated && courseId) {
// Data has been sent to server. Now invalidate the WS calls.
const promises = [];
promises.push(this.lessonProvider.invalidateAccessInformation(lessonId, siteId));
promises.push(this.lessonProvider.invalidateContentPagesViewed(lessonId, siteId));
promises.push(this.lessonProvider.invalidateQuestionsAttempts(lessonId, siteId));
promises.push(this.lessonProvider.invalidatePagesPossibleJumps(lessonId, siteId));
promises.push(this.lessonProvider.invalidateTimers(lessonId, siteId));
return this.utils.allPromises(promises).catch(() => {
// Ignore errors.
}).then(() => {
// Sync successful, update some data that might have been modified.
return this.lessonProvider.getAccessInformation(lessonId, false, false, siteId).then((info) => {
const promises = [],
retake = info.attemptscount;
promises.push(this.lessonProvider.getContentPagesViewedOnline(lessonId, retake, false, false, siteId));
promises.push(this.lessonProvider.getQuestionsAttemptsOnline(lessonId, retake, false, undefined, false,
false, siteId));
return Promise.all(promises);
}).catch(() => {
// Ignore errors.
});
});
}
}).then(() => {
// Sync finished, set sync time.
return this.setSyncTime(lessonId, siteId).catch(() => {
// Ignore errors.
});
}).then(() => {
// All done, return the result.
return result;
});
return this.addOngoingSync(lessonId, syncPromise, siteId);
}
/**
* Send an attempt to the site and delete it afterwards.
*
* @param {any} lesson Lesson.
* @param {string} password Password (if any).
* @param {any} attempt Attempt to send.
* @param {AddonModLessonSyncResult} result Result where to store the data.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when done.
*/
protected sendAttempt(lesson: any, password: string, attempt: any, result: AddonModLessonSyncResult, siteId?: string)
: Promise<any> {
return this.lessonProvider.processPageOnline(lesson.id, attempt.pageid, attempt.data, password, false, siteId).then(() => {
result.updated = true;
return this.lessonOfflineProvider.deleteAttempt(lesson.id, attempt.retake, attempt.pageid, attempt.timemodified,
siteId);
}).catch((error) => {
if (error && this.utils.isWebServiceError(error)) {
// The WebService has thrown an error, this means that the attempt cannot be submitted. Delete it.
result.updated = true;
return this.lessonOfflineProvider.deleteAttempt(lesson.id, attempt.retake, attempt.pageid, attempt.timemodified,
siteId).then(() => {
// Attempt deleted, add a warning.
result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
component: this.componentTranslate,
name: lesson.name,
error: error
}));
});
} else {
// Couldn't connect to server, reject.
return Promise.reject(error);
}
});
}
}

View File

@ -0,0 +1,449 @@
// (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, Injector } from '@angular/core';
import { ModalController } from 'ionic-angular';
import { CoreGroupsProvider } from '@providers/groups';
import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler';
import { AddonModLessonProvider } from './lesson';
/**
* Handler to prefetch lessons.
*/
@Injectable()
export class AddonModLessonPrefetchHandler extends CoreCourseModulePrefetchHandlerBase {
name = 'AddonModLesson';
modName = 'lesson';
component = AddonModLessonProvider.COMPONENT;
// Don't check timers to decrease positives. If a user performs some action it will be reflected in other items.
updatesNames = /^configuration$|^.*files$|^grades$|^gradeitems$|^pages$|^answers$|^questionattempts$|^pagesviewed$/;
constructor(protected injector: Injector, protected modalCtrl: ModalController, protected groupsProvider: CoreGroupsProvider,
protected lessonProvider: AddonModLessonProvider) {
super(injector);
}
/**
* Ask password.
*
* @param {any} info Lesson access info.
* @return {Promise<string>} Promise resolved with the password.
*/
protected askUserPassword(info: any): Promise<string> {
// Create and show the modal.
const modal = this.modalCtrl.create('AddonModLessonPasswordModalPage');
modal.present();
// Wait for modal to be dismissed.
return new Promise((resolve, reject): void => {
modal.onDidDismiss((password) => {
if (typeof password != 'undefined') {
resolve(password);
} else {
reject(this.domUtils.createCanceledError());
}
});
});
}
/**
* 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. @see downloadOrPrefetch.
* @return {Promise<any>} Promise resolved when all content is downloaded.
*/
download(module: any, courseId: number, dirPath?: string): Promise<any> {
// Same implementation for download and prefetch.
return this.prefetch(module, courseId, false, dirPath);
}
/**
* Get the download size of a module.
*
* @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.
* @return {Promise<{size: number, total: boolean}>} Promise resolved with the size and a boolean indicating if it was able
* to calculate the total size.
*/
getDownloadSize(module: any, courseId: any, single?: boolean): Promise<{ size: number, total: boolean }> {
const siteId = this.sitesProvider.getCurrentSiteId();
let lesson,
password,
result;
return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lessonData) => {
lesson = lessonData;
// Get the lesson password if it's needed.
return this.getLessonPassword(lesson.id, false, true, single, siteId);
}).then((data) => {
password = data.password;
lesson = data.lesson || lesson;
// Get intro files and media files.
let files = lesson.mediafiles || [];
files = files.concat(this.getIntroFilesFromInstance(module, lesson));
result = this.utils.sumFileSizes(files);
// Get the pages to calculate the size.
return this.lessonProvider.getPages(lesson.id, password, false, false, siteId);
}).then((pages) => {
pages.forEach((page) => {
result.size += page.filessizetotal;
});
return result;
});
}
/**
* Get list of files. If not defined, we'll assume they're in module.contents.
*
* @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.
* @return {Promise<any[]>} Promise resolved with the list of files.
*/
getFiles(module: any, courseId: number, single?: boolean): Promise<any[]> {
return Promise.resolve([]);
}
/**
* Get the lesson password if needed. If not stored, it can ask the user to enter it.
*
* @param {number} lessonId Lesson ID.
* @param {boolean} [forceCache] Whether it should return cached data. Has priority over ignoreCache.
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
* @param {boolean} [askPassword] True if we should ask for password if needed, false otherwise.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<{password?: string, lesson?: any, accessInfo: any}>} Promise resolved when done.
*/
getLessonPassword(lessonId: number, forceCache?: boolean, ignoreCache?: boolean, askPassword?: boolean, siteId?: string)
: Promise<{password?: string, lesson?: any, accessInfo: any}> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
// Get access information to check if password is needed.
return this.lessonProvider.getAccessInformation(lessonId, forceCache, ignoreCache, siteId).then((info): any => {
if (info.preventaccessreasons && info.preventaccessreasons.length) {
const passwordNeeded = info.preventaccessreasons.length == 1 && this.lessonProvider.isPasswordProtected(info);
if (passwordNeeded) {
// The lesson requires a password. Check if there is one in DB.
return this.lessonProvider.getStoredPassword(lessonId).catch(() => {
// No password found.
}).then((password) => {
if (password) {
return this.validatePassword(lessonId, info, password, forceCache, ignoreCache, siteId);
} else {
return Promise.reject(null);
}
}).catch(() => {
// No password or error validating it. Ask for it if allowed.
if (askPassword) {
return this.askUserPassword(info).then((password) => {
return this.validatePassword(lessonId, info, password, forceCache, ignoreCache, siteId);
});
}
// Cannot ask for password, reject.
return Promise.reject(info.preventaccessreasons[0].message);
});
} else {
// Lesson cannot be played, reject.
return Promise.reject(info.preventaccessreasons[0].message);
}
}
// Password not needed.
return { accessInfo: info };
});
}
/**
* Invalidate the prefetched content.
*
* @param {number} moduleId The module ID.
* @param {number} courseId The course ID the module belongs to.
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateContent(moduleId: number, courseId: number): Promise<any> {
// Only invalidate the data that doesn't ignore cache when prefetching.
const promises = [];
promises.push(this.lessonProvider.invalidateLessonData(courseId));
promises.push(this.courseProvider.invalidateModule(moduleId));
promises.push(this.groupsProvider.invalidateActivityAllowedGroups(moduleId));
return Promise.all(promises);
}
/**
* Invalidate WS calls needed to determine module status.
*
* @param {any} module Module.
* @param {number} courseId Course ID the module belongs to.
* @return {Promise<any>} Promise resolved when invalidated.
*/
invalidateModule(module: any, courseId: number): Promise<any> {
const siteId = this.sitesProvider.getCurrentSiteId();
// Invalidate data to determine if module is downloadable.
return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lesson) => {
const promises = [];
promises.push(this.lessonProvider.invalidateLessonData(courseId, siteId));
promises.push(this.lessonProvider.invalidateAccessInformation(lesson.id, siteId));
return Promise.all(promises);
});
}
/**
* Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable.
*
* @param {any} module Module.
* @param {number} courseId Course ID the module belongs to.
* @return {boolean|Promise<boolean>} Whether the module can be downloaded. The promise should never be rejected.
*/
isDownloadable(module: any, courseId: number): boolean | Promise<boolean> {
const siteId = this.sitesProvider.getCurrentSiteId();
return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lesson) => {
if (!this.lessonProvider.isLessonOffline(lesson)) {
return false;
}
// Check if there is any prevent access reason.
return this.lessonProvider.getAccessInformation(lesson.id, false, false, siteId).then((info) => {
// It's downloadable if there are no prevent access reasons or there is just 1 and it's password.
return !info.preventaccessreasons || !info.preventaccessreasons.length ||
(info.preventaccessreasons.length == 1 && this.lessonProvider.isPasswordProtected(info));
});
});
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return {boolean|Promise<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
*/
isEnabled(): boolean | Promise<boolean> {
return this.lessonProvider.isPluginEnabled();
}
/**
* Prefetch a module.
*
* @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. @see downloadOrPrefetch.
* @return {Promise<any>} Promise resolved when done.
*/
prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> {
return this.prefetchPackage(module, courseId, single, this.prefetchLesson.bind(this));
}
/**
* Prefetch a lesson.
*
* @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} siteId Site ID.
* @return {Promise<any>} Promise resolved when done.
*/
protected prefetchLesson(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
let lesson,
password,
accessInfo;
return this.lessonProvider.getLesson(courseId, module.id, false, siteId).then((lessonData) => {
lesson = lessonData;
// Get the lesson password if it's needed.
return this.getLessonPassword(lesson.id, false, true, single, siteId);
}).then((data) => {
password = data.password;
lesson = data.lesson || lesson;
accessInfo = data.accessInfo;
if (!this.lessonProvider.leftDuringTimed(accessInfo)) {
// The user didn't left during a timed session. Call launch retake to make sure there is a started retake.
return this.lessonProvider.launchRetake(lesson.id, password, undefined, false, siteId).then(() => {
const promises = [];
// New data generated, update the download time and refresh the access info.
promises.push(this.filepoolProvider.updatePackageDownloadTime(siteId, this.component, module.id).catch(() => {
// Ignore errors.
}));
promises.push(this.lessonProvider.getAccessInformation(lesson.id, false, true, siteId).then((info) => {
accessInfo = info;
}));
return Promise.all(promises);
});
}
}).then(() => {
const promises = [],
retake = accessInfo.attemptscount;
// Download intro files and media files.
let files = lesson.mediafiles || [];
files = files.concat(this.getIntroFilesFromInstance(module, lesson));
promises.push(this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id));
// Get the list of pages.
promises.push(this.lessonProvider.getPages(lesson.id, password, false, true, siteId).then((pages) => {
const subPromises = [];
let hasRandomBranch = false;
// Get the data for each page.
pages.forEach((data) => {
// Check if any page has a RANDOMBRANCH jump.
if (!hasRandomBranch) {
for (let i = 0; i < data.jumps.length; i++) {
if (data.jumps[i] == AddonModLessonProvider.LESSON_RANDOMBRANCH) {
hasRandomBranch = true;
break;
}
}
}
// Get the page data. We don't pass accessInfo because we don't need to calculate the offline data.
subPromises.push(this.lessonProvider.getPageData(lesson, data.page.id, password, false, true, false,
true, undefined, undefined, siteId).then((pageData) => {
// Download the page files.
let pageFiles = pageData.contentfiles || [];
pageData.answers.forEach((answer) => {
if (answer.answerfiles && answer.answerfiles.length) {
pageFiles = pageFiles.concat(answer.answerfiles);
}
if (answer.responsefiles && answer.responsefiles.length) {
pageFiles = pageFiles.concat(answer.responsefiles);
}
});
return this.filepoolProvider.addFilesToQueue(siteId, pageFiles, this.component, module.id);
}));
});
// Prefetch the list of possible jumps for offline navigation. Do it here because we know hasRandomBranch.
subPromises.push(this.lessonProvider.getPagesPossibleJumps(lesson.id, false, true, siteId).catch((error) => {
if (hasRandomBranch) {
// The WebSevice probably failed because RANDOMBRANCH aren't supported if the user hasn't seen any page.
return Promise.reject(this.translate.instant('addon.mod_lesson.errorprefetchrandombranch'));
} else {
return Promise.reject(error);
}
}));
return Promise.all(subPromises);
}));
// Prefetch user timers to be able to calculate timemodified in offline.
promises.push(this.lessonProvider.getTimers(lesson.id, false, true, siteId).catch(() => {
// Ignore errors.
}));
// Prefetch viewed pages in last retake to calculate progress.
promises.push(this.lessonProvider.getContentPagesViewedOnline(lesson.id, retake, false, true, siteId));
// Prefetch question attempts in last retake for offline calculations.
promises.push(this.lessonProvider.getQuestionsAttemptsOnline(lesson.id, retake, false, undefined, false, true, siteId));
// Get module info to be able to handle links.
promises.push(this.courseProvider.getModuleBasicInfo(module.id, siteId));
if (accessInfo.canviewreports) {
// Prefetch reports data.
promises.push(this.groupsProvider.getActivityAllowedGroupsIfEnabled(module.id, undefined, siteId).then((groups) => {
const subPromises = [];
groups.forEach((group) => {
subPromises.push(this.lessonProvider.getRetakesOverview(lesson.id, group.id, false, true, siteId));
});
// Always get group 0, even if there are no groups.
subPromises.push(this.lessonProvider.getRetakesOverview(lesson.id, 0, false, true, siteId).then((data) => {
if (!data || !data.students) {
return;
}
// Prefetch the last retake for each user.
const retakePromises = [];
data.students.forEach((student) => {
if (!student.attempts || !student.attempts.length) {
return;
}
const lastRetake = student.attempts[student.attempts.length - 1];
if (!lastRetake) {
return;
}
retakePromises.push(this.lessonProvider.getUserRetake(lesson.id, lastRetake.try, student.id, false,
true, siteId));
});
return Promise.all(retakePromises);
}));
return Promise.all(subPromises);
}));
}
return Promise.all(promises);
});
}
/**
* Validate the password.
*
* @param {number} lessonId Lesson ID.
* @param {any} info Lesson access info.
* @param {string} pwd Password to check.
* @param {boolean} [forceCache] Whether it should return cached data. Has priority over ignoreCache.
* @param {boolean} [ignoreCache] Whether it should ignore cached data (it will always fail in offline or server down).
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<{password: string, lesson: any, accessInfo: any}>} Promise resolved when done.
*/
protected validatePassword(lessonId: number, info: any, pwd: string, forceCache?: boolean, ignoreCache?: boolean,
siteId?: string): Promise<{password: string, lesson: any, accessInfo: any}> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
return this.lessonProvider.getLessonWithPassword(lessonId, pwd, true, forceCache, ignoreCache, siteId).then((lesson) => {
// Password is ok, store it and return the data.
return this.lessonProvider.storePassword(lesson.id, pwd, siteId).then(() => {
return {
password: pwd,
lesson: lesson,
accessInfo: info
};
});
});
}
}

View File

@ -33,7 +33,7 @@
<form [formGroup]="credForm" (ngSubmit)="login()">
<ion-item>
<core-show-password item-content [name]="'password'">
<ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" core-show-password core-auto-focus></ion-input>
<ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" core-auto-focus></ion-input>
</core-show-password>
</ion-item>
<ion-grid>