MOBILE-3648 lesson: Implement sync service and prefetch handler
parent
2ff5883026
commit
f1fbb75889
|
@ -0,0 +1,39 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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 { CoreSharedModule } from '@/core/shared.module';
|
||||
import { AddonModLessonPasswordModalComponent } from './password-modal/password-modal';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModLessonPasswordModalComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreSharedModule,
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [
|
||||
AddonModLessonPasswordModalComponent,
|
||||
],
|
||||
})
|
||||
export class AddonModLessonComponentsModule {}
|
|
@ -0,0 +1,28 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>{{ 'core.login.password' | translate }}</ion-title>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
<core-icon slot="icon-only" name="fas-times"></core-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding addon-mod_lesson-password-modal">
|
||||
<form (ngSubmit)="submitPassword($event, passwordinput)" #passwordForm>
|
||||
<ion-item>
|
||||
<ion-label>{{ 'addon.mod_lesson.enterpassword' | translate }}</ion-label>
|
||||
<core-show-password name="password">
|
||||
<ion-input name="password" type="password" placeholder="{{ 'core.login.password' | translate }}"
|
||||
[core-auto-focus] #passwordinput [clearOnEdit]="false"></ion-input>
|
||||
</core-show-password>
|
||||
</ion-item>
|
||||
<ion-button expand="block" type="submit">
|
||||
{{ 'addon.mod_lesson.continue' | translate }}
|
||||
<core-icon slot="end" name="fas-arrow-right"></core-icon>
|
||||
</ion-button>
|
||||
<!-- Remove this once Ionic fixes this bug: https://github.com/ionic-team/ionic-framework/issues/19368 -->
|
||||
<input type="submit" class="core-submit-hidden-enter" />
|
||||
</form>
|
||||
</ion-content>
|
|
@ -0,0 +1,57 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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, ElementRef } from '@angular/core';
|
||||
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { ModalController } from '@singletons';
|
||||
|
||||
|
||||
/**
|
||||
* Modal that asks the password for a lesson.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-addon-mod-lesson-password-modal',
|
||||
templateUrl: 'password-modal.html',
|
||||
})
|
||||
export class AddonModLessonPasswordModalComponent {
|
||||
|
||||
@ViewChild('passwordForm') formElement?: ElementRef;
|
||||
|
||||
/**
|
||||
* Send the password back.
|
||||
*
|
||||
* @param e Event.
|
||||
* @param password The input element.
|
||||
*/
|
||||
submitPassword(e: Event, password: HTMLInputElement): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, false, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
ModalController.instance.dismiss(password.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal.
|
||||
*/
|
||||
closeModal(): void {
|
||||
CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
ModalController.instance.dismiss();
|
||||
}
|
||||
|
||||
}
|
|
@ -12,13 +12,19 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
|
||||
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
|
||||
import { CoreCronDelegate } from '@services/cron';
|
||||
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||
import { AddonModLessonComponentsModule } from './components/components.module';
|
||||
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/lesson';
|
||||
import { AddonModLessonPrefetchHandler } from './services/handlers/prefetch';
|
||||
import { AddonModLessonSyncCronHandler } from './services/handlers/sync-cron';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AddonModLessonComponentsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
@ -26,6 +32,15 @@ import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/lesson';
|
|||
useValue: [SITE_SCHEMA, OFFLINE_SITE_SCHEMA],
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
deps: [],
|
||||
useFactory: () => () => {
|
||||
CoreCourseModulePrefetchDelegate.instance.registerHandler(AddonModLessonPrefetchHandler.instance);
|
||||
CoreCronDelegate.instance.register(AddonModLessonSyncCronHandler.instance);
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AddonModLessonModule {}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
import { CoreSiteSchema } from '@services/sites';
|
||||
|
||||
/**
|
||||
* Database variables for AddonModLessonOfflineProvider.
|
||||
* Database variables for AddonModLessonProvider.
|
||||
*/
|
||||
export const PASSWORD_TABLE_NAME = 'addon_mod_lesson_password';
|
||||
export const SITE_SCHEMA: CoreSiteSchema = {
|
||||
|
@ -145,6 +145,39 @@ export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
|
|||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Database variables for AddonModLessonSyncProvider.
|
||||
*/
|
||||
export const RETAKES_FINISHED_SYNC_TABLE_NAME = 'addon_mod_lesson_retakes_finished_sync';
|
||||
export const SYNC_SITE_SCHEMA: CoreSiteSchema = {
|
||||
name: 'AddonModLessonSyncProvider',
|
||||
version: 1,
|
||||
tables: [
|
||||
{
|
||||
name: RETAKES_FINISHED_SYNC_TABLE_NAME,
|
||||
columns: [
|
||||
{
|
||||
name: 'lessonid',
|
||||
type: 'INTEGER',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
name: 'retake',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'pageid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'timefinished',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Lesson retake data.
|
||||
*/
|
||||
|
@ -183,3 +216,13 @@ export type AddonModLessonPageAttemptDBRecord = {
|
|||
answerid: number | null;
|
||||
useranswer: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data about a retake finished in sync.
|
||||
*/
|
||||
export type AddonModLessonRetakeFinishedInSyncDBRecord = {
|
||||
lessonid: number;
|
||||
retake: number;
|
||||
pageid: number;
|
||||
timefinished: number;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,576 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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 { CoreCanceledError } from '@classes/errors/cancelederror';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
|
||||
import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
|
||||
import { CoreCourse, CoreCourseCommonModWSOptions, CoreCourseAnyModuleData } from '@features/course/services/course';
|
||||
import { CoreFilepool } from '@services/filepool';
|
||||
import { CoreGroups } from '@services/groups';
|
||||
import { CoreFileSizeSum, CorePluginFileDelegate } from '@services/plugin-file-delegate';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
import { makeSingleton, ModalController, Translate } from '@singletons';
|
||||
import { AddonModLessonPasswordModalComponent } from '../../components/password-modal/password-modal';
|
||||
import {
|
||||
AddonModLesson,
|
||||
AddonModLessonGetAccessInformationWSResponse,
|
||||
AddonModLessonLessonWSData,
|
||||
AddonModLessonPasswordOptions,
|
||||
AddonModLessonProvider,
|
||||
} from '../lesson';
|
||||
import { AddonModLessonSync, AddonModLessonSyncResult } from '../lesson-sync';
|
||||
|
||||
/**
|
||||
* Handler to prefetch lessons.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModLessonPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
|
||||
|
||||
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$/;
|
||||
|
||||
/**
|
||||
* Ask password.
|
||||
*
|
||||
* @return Promise resolved with the password.
|
||||
*/
|
||||
protected async askUserPassword(): Promise<string> {
|
||||
// Create and show the modal.
|
||||
const modal = await ModalController.instance.create({
|
||||
component: AddonModLessonPasswordModalComponent,
|
||||
});
|
||||
|
||||
await modal.present();
|
||||
|
||||
const password = <string | undefined> await modal.onWillDismiss();
|
||||
|
||||
if (typeof password != 'string') {
|
||||
throw new CoreCanceledError();
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the download size of a module.
|
||||
*
|
||||
* @param module Module.
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||
* @return Promise resolved with the size.
|
||||
*/
|
||||
async getDownloadSize(module: CoreCourseAnyModuleData, courseId: number, single?: boolean): Promise<CoreFileSizeSum> {
|
||||
const siteId = CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
let lesson = await AddonModLesson.instance.getLesson(courseId, module.id, { siteId });
|
||||
|
||||
// Get the lesson password if it's needed.
|
||||
const passwordData = await this.getLessonPassword(lesson.id, {
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
askPassword: single,
|
||||
siteId,
|
||||
});
|
||||
|
||||
lesson = passwordData.lesson || lesson;
|
||||
|
||||
// Get intro files and media files.
|
||||
let files = lesson.mediafiles || [];
|
||||
files = files.concat(this.getIntroFilesFromInstance(module, lesson));
|
||||
|
||||
const result = await CorePluginFileDelegate.instance.getFilesDownloadSize(files);
|
||||
|
||||
// Get the pages to calculate the size.
|
||||
const pages = await AddonModLesson.instance.getPages(lesson.id, {
|
||||
cmId: module.id,
|
||||
password: passwordData.password,
|
||||
siteId,
|
||||
});
|
||||
|
||||
pages.forEach((page) => {
|
||||
result.size += page.filessizetotal;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lesson password if needed. If not stored, it can ask the user to enter it.
|
||||
*
|
||||
* @param lessonId Lesson ID.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async getLessonPassword(
|
||||
lessonId: number,
|
||||
options: AddonModLessonGetPasswordOptions = {},
|
||||
): Promise<AddonModLessonGetPasswordResult> {
|
||||
|
||||
options.siteId = options.siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
// Get access information to check if password is needed.
|
||||
const accessInfo = await AddonModLesson.instance.getAccessInformation(lessonId, options);
|
||||
|
||||
if (!accessInfo.preventaccessreasons.length) {
|
||||
// Password not needed.
|
||||
return { accessInfo };
|
||||
}
|
||||
|
||||
const passwordNeeded = accessInfo.preventaccessreasons.length == 1 &&
|
||||
AddonModLesson.instance.isPasswordProtected(accessInfo);
|
||||
|
||||
if (!passwordNeeded) {
|
||||
// Lesson cannot be played, reject.
|
||||
throw new CoreError(accessInfo.preventaccessreasons[0].message);
|
||||
}
|
||||
|
||||
// The lesson requires a password. Check if there is one in DB.
|
||||
let password = await CoreUtils.instance.ignoreErrors(AddonModLesson.instance.getStoredPassword(lessonId));
|
||||
|
||||
if (password) {
|
||||
try {
|
||||
return this.validatePassword(lessonId, accessInfo, password, options);
|
||||
} catch {
|
||||
// Error validating it.
|
||||
}
|
||||
}
|
||||
|
||||
// Ask for the password if allowed.
|
||||
if (!options.askPassword) {
|
||||
// Cannot ask for password, reject.
|
||||
throw new CoreError(accessInfo.preventaccessreasons[0].message);
|
||||
}
|
||||
|
||||
password = await this.askUserPassword();
|
||||
|
||||
return this.validatePassword(lessonId, accessInfo, password, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the prefetched content.
|
||||
*
|
||||
* @param moduleId The module ID.
|
||||
* @param courseId The course ID the module belongs to.
|
||||
* @return Promise resolved when the data is invalidated.
|
||||
*/
|
||||
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
|
||||
// Only invalidate the data that doesn't ignore cache when prefetching.
|
||||
await Promise.all([
|
||||
AddonModLesson.instance.invalidateLessonData(courseId),
|
||||
CoreCourse.instance.invalidateModule(moduleId),
|
||||
CoreGroups.instance.invalidateActivityAllowedGroups(moduleId),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate WS calls needed to determine module status.
|
||||
*
|
||||
* @param module Module.
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @return Promise resolved when invalidated.
|
||||
*/
|
||||
async invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise<void> {
|
||||
// Invalidate data to determine if module is downloadable.
|
||||
const siteId = CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
const lesson = await AddonModLesson.instance.getLesson(courseId, module.id, {
|
||||
readingStrategy: CoreSitesReadingStrategy.PreferCache,
|
||||
siteId,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
AddonModLesson.instance.invalidateLessonData(courseId, siteId),
|
||||
AddonModLesson.instance.invalidateAccessInformation(lesson.id, siteId),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable.
|
||||
*
|
||||
* @param module Module.
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @return Whether the module can be downloaded. The promise should never be rejected.
|
||||
*/
|
||||
async isDownloadable(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> {
|
||||
const siteId = CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
const lesson = await AddonModLesson.instance.getLesson(courseId, module.id, { siteId });
|
||||
const accessInfo = await AddonModLesson.instance.getAccessInformation(lesson.id, { cmId: module.id, siteId });
|
||||
|
||||
// If it's a student and lesson isn't offline, it isn't downloadable.
|
||||
if (!accessInfo.canviewreports && !AddonModLesson.instance.isLessonOffline(lesson)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// It's downloadable if there are no prevent access reasons or there is just 1 and it's password.
|
||||
return !accessInfo.preventaccessreasons.length ||
|
||||
(accessInfo.preventaccessreasons.length == 1 && AddonModLesson.instance.isPasswordProtected(accessInfo));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return Promise resolved with a boolean indicating if the handler is enabled.
|
||||
*/
|
||||
isEnabled(): Promise<boolean> {
|
||||
return AddonModLesson.instance.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch a module.
|
||||
*
|
||||
* @param module Module.
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||
* @param dirPath Path of the directory where to store all the content files.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
prefetch(module: CoreCourseAnyModuleData, courseId?: number, single?: boolean, dirPath?: string): Promise<void> {
|
||||
return this.prefetchPackage(module, courseId, this.prefetchLesson.bind(this, module, courseId, single));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch a lesson.
|
||||
*
|
||||
* @param module Module.
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async prefetchLesson(module: CoreCourseAnyModuleData, courseId?: number, single?: boolean): Promise<void> {
|
||||
const siteId = CoreSites.instance.getCurrentSiteId();
|
||||
courseId = courseId || module.course || 1;
|
||||
|
||||
const commonOptions = {
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
siteId,
|
||||
};
|
||||
const modOptions = {
|
||||
cmId: module.id,
|
||||
...commonOptions, // Include all common options.
|
||||
};
|
||||
|
||||
let lesson = await AddonModLesson.instance.getLesson(courseId, module.id, commonOptions);
|
||||
|
||||
// Get the lesson password if it's needed.
|
||||
const passwordData = await this.getLessonPassword(lesson.id, {
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
askPassword: single,
|
||||
siteId,
|
||||
});
|
||||
|
||||
lesson = passwordData.lesson || lesson;
|
||||
let accessInfo = passwordData.accessInfo;
|
||||
const password = passwordData.password;
|
||||
|
||||
if (AddonModLesson.instance.isLessonOffline(lesson) && !AddonModLesson.instance.leftDuringTimed(accessInfo)) {
|
||||
// The user didn't left during a timed session. Call launch retake to make sure there is a started retake.
|
||||
accessInfo = await this.launchRetake(lesson.id, password, modOptions, siteId);
|
||||
}
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// Download intro files and media files.
|
||||
const files = (lesson.mediafiles || []).concat(this.getIntroFilesFromInstance(module, lesson));
|
||||
promises.push(CoreFilepool.instance.addFilesToQueue(siteId, files, this.component, module.id));
|
||||
|
||||
if (AddonModLesson.instance.isLessonOffline(lesson)) {
|
||||
promises.push(this.prefetchPlayData(lesson, password, accessInfo.attemptscount, modOptions));
|
||||
}
|
||||
|
||||
if (accessInfo.canviewreports) {
|
||||
promises.push(this.prefetchGroupInfo(module.id, lesson.id, modOptions));
|
||||
promises.push(this.prefetchReportsData(module.id, lesson.id, modOptions));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a retake and return the updated access information.
|
||||
*
|
||||
* @param lessonId Lesson ID.
|
||||
* @param password Password (if needed).
|
||||
* @param modOptions Options.
|
||||
* @param siteId Site ID.
|
||||
*/
|
||||
protected async launchRetake(
|
||||
lessonId: number,
|
||||
password: string | undefined,
|
||||
modOptions: CoreCourseCommonModWSOptions,
|
||||
siteId: string,
|
||||
): Promise<AddonModLessonGetAccessInformationWSResponse> {
|
||||
// The user didn't left during a timed session. Call launch retake to make sure there is a started retake.
|
||||
await AddonModLesson.instance.launchRetake(lessonId, password, undefined, false, siteId);
|
||||
|
||||
const results = await Promise.all([
|
||||
CoreUtils.instance.ignoreErrors(CoreFilepool.instance.updatePackageDownloadTime(siteId, this.component, module.id)),
|
||||
AddonModLesson.instance.getAccessInformation(lessonId, modOptions),
|
||||
]);
|
||||
|
||||
return results[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch data to play the lesson in offline.
|
||||
*
|
||||
* @param lesson Lesson.
|
||||
* @param password Password (if needed).
|
||||
* @param retake Retake to prefetch.
|
||||
* @param options Options.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async prefetchPlayData(
|
||||
lesson: AddonModLessonLessonWSData,
|
||||
password: string | undefined,
|
||||
retake: number,
|
||||
modOptions: CoreCourseCommonModWSOptions,
|
||||
): Promise<void> {
|
||||
const passwordOptions = {
|
||||
password,
|
||||
...modOptions, // Include all mod options.
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
this.prefetchPagesData(lesson, passwordOptions),
|
||||
// Prefetch user timers to be able to calculate timemodified in offline.
|
||||
CoreUtils.instance.ignoreErrors(AddonModLesson.instance.getTimers(lesson.id, modOptions)),
|
||||
// Prefetch viewed pages in last retake to calculate progress.
|
||||
AddonModLesson.instance.getContentPagesViewedOnline(lesson.id, retake, modOptions),
|
||||
// Prefetch question attempts in last retake for offline calculations.
|
||||
AddonModLesson.instance.getQuestionsAttemptsOnline(lesson.id, retake, modOptions),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch data related to pages.
|
||||
*
|
||||
* @param lesson Lesson.
|
||||
* @param options Options.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async prefetchPagesData(
|
||||
lesson: AddonModLessonLessonWSData,
|
||||
options: AddonModLessonPasswordOptions,
|
||||
): Promise<void> {
|
||||
const pages = await AddonModLesson.instance.getPages(lesson.id, options);
|
||||
|
||||
let hasRandomBranch = false;
|
||||
|
||||
// Get the data for each page.
|
||||
const promises = pages.map(async (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.
|
||||
const pageData = await AddonModLesson.instance.getPageData(lesson, data.page.id, {
|
||||
includeContents: true,
|
||||
includeOfflineData: false,
|
||||
...options, // Include all options.
|
||||
});
|
||||
|
||||
// Download the page files.
|
||||
let pageFiles = pageData.contentfiles || [];
|
||||
|
||||
pageData.answers.forEach((answer) => {
|
||||
pageFiles = pageFiles.concat(answer.answerfiles);
|
||||
pageFiles = pageFiles.concat(answer.responsefiles);
|
||||
});
|
||||
|
||||
await CoreFilepool.instance.addFilesToQueue(options.siteId!, pageFiles, this.component, module.id);
|
||||
});
|
||||
|
||||
// Prefetch the list of possible jumps for offline navigation. Do it here because we know hasRandomBranch.
|
||||
promises.push(this.prefetchPossibleJumps(lesson.id, hasRandomBranch, options));
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch possible jumps.
|
||||
*
|
||||
* @param lessonId Lesson ID.
|
||||
* @param hasRandomBranch Whether any page has a random branch jump.
|
||||
* @param modOptions Options.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async prefetchPossibleJumps(
|
||||
lessonId: number,
|
||||
hasRandomBranch: boolean,
|
||||
modOptions: CoreCourseCommonModWSOptions,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await AddonModLesson.instance.getPagesPossibleJumps(lessonId, modOptions);
|
||||
} catch (error) {
|
||||
if (hasRandomBranch) {
|
||||
// The WebSevice probably failed because RANDOMBRANCH aren't supported if the user hasn't seen any page.
|
||||
throw new CoreError(Translate.instance.instant('addon.mod_lesson.errorprefetchrandombranch'));
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch group info.
|
||||
*
|
||||
* @param moduleId Module ID.
|
||||
* @param lessonId Lesson ID.
|
||||
* @param modOptions Options.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async prefetchGroupInfo(
|
||||
moduleId: number,
|
||||
lessonId: number,
|
||||
modOptions: CoreCourseCommonModWSOptions,
|
||||
): Promise<void> {
|
||||
const groupInfo = await CoreGroups.instance.getActivityGroupInfo(moduleId, false, undefined, modOptions.siteId, true);
|
||||
|
||||
await Promise.all(groupInfo.groups?.map(async (group) => {
|
||||
await AddonModLesson.instance.getRetakesOverview(lessonId, {
|
||||
groupId: group.id,
|
||||
...modOptions, // Include all options.
|
||||
});
|
||||
}) || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch reports data.
|
||||
*
|
||||
* @param moduleId Module ID.
|
||||
* @param lessonId Lesson ID.
|
||||
* @param modOptions Options.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async prefetchReportsData(
|
||||
moduleId: number,
|
||||
lessonId: number,
|
||||
modOptions: CoreCourseCommonModWSOptions,
|
||||
): Promise<void> {
|
||||
// Always get all participants, even if there are no groups.
|
||||
const data = await AddonModLesson.instance.getRetakesOverview(lessonId, modOptions);
|
||||
if (!data || !data.students) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefetch the last retake for each user.
|
||||
await Promise.all(data.students.map(async (student) => {
|
||||
const lastRetake = student.attempts?.[student.attempts.length - 1];
|
||||
if (!lastRetake) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attempt = await AddonModLesson.instance.getUserRetake(lessonId, lastRetake.try, {
|
||||
userId: student.id,
|
||||
...modOptions, // Include all options.
|
||||
});
|
||||
|
||||
if (!attempt?.answerpages) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Download embedded files in essays.
|
||||
const files: CoreWSExternalFile[] = [];
|
||||
attempt.answerpages.forEach((answerPage) => {
|
||||
if (!answerPage.page || answerPage.page.qtype != AddonModLessonProvider.LESSON_PAGE_ESSAY) {
|
||||
return;
|
||||
}
|
||||
|
||||
answerPage.answerdata?.answers?.forEach((answer) => {
|
||||
files.push(...CoreFilepool.instance.extractDownloadableFilesFromHtmlAsFakeFileObjects(answer[0]));
|
||||
});
|
||||
});
|
||||
|
||||
await CoreFilepool.instance.addFilesToQueue(modOptions.siteId!, files, this.component, moduleId);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the password.
|
||||
*
|
||||
* @param lessonId Lesson ID.
|
||||
* @param info Lesson access info.
|
||||
* @param pwd Password to check.
|
||||
* @param options Other options.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async validatePassword(
|
||||
lessonId: number,
|
||||
accessInfo: AddonModLessonGetAccessInformationWSResponse,
|
||||
password: string,
|
||||
options: CoreCourseCommonModWSOptions = {},
|
||||
): Promise<AddonModLessonGetPasswordResult> {
|
||||
|
||||
options.siteId = options.siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
const lesson = await AddonModLesson.instance.getLessonWithPassword(lessonId, {
|
||||
password,
|
||||
...options, // Include all options.
|
||||
});
|
||||
|
||||
// Password is ok, store it and return the data.
|
||||
await AddonModLesson.instance.storePassword(lesson.id, password, options.siteId);
|
||||
|
||||
return {
|
||||
password,
|
||||
lesson,
|
||||
accessInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a module.
|
||||
*
|
||||
* @param module Module.
|
||||
* @param courseId Course ID the module belongs to
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise<AddonModLessonSyncResult> {
|
||||
return AddonModLessonSync.instance.syncLesson(module.instance!, false, false, siteId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class AddonModLessonPrefetchHandler extends makeSingleton(AddonModLessonPrefetchHandlerService) {}
|
||||
|
||||
/**
|
||||
* Options to pass to get lesson password.
|
||||
*/
|
||||
export type AddonModLessonGetPasswordOptions = CoreCourseCommonModWSOptions & {
|
||||
askPassword?: boolean; // True if we should ask for password if needed, false otherwise.
|
||||
};
|
||||
|
||||
/**
|
||||
* Result of getLessonPassword.
|
||||
*/
|
||||
export type AddonModLessonGetPasswordResult = {
|
||||
password?: string;
|
||||
lesson?: AddonModLessonLessonWSData;
|
||||
accessInfo: AddonModLessonGetAccessInformationWSResponse;
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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 { CoreCronHandler } from '@services/cron';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonModLessonSync } from '../lesson-sync';
|
||||
|
||||
/**
|
||||
* Synchronization cron handler.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModLessonSyncCronHandlerService implements CoreCronHandler {
|
||||
|
||||
name = 'AddonModLessonSyncCronHandler';
|
||||
|
||||
/**
|
||||
* Execute the process.
|
||||
* Receives the ID of the site affected, undefined for all sites.
|
||||
*
|
||||
* @param siteId ID of the site affected, undefined for all sites.
|
||||
* @param force Wether the execution is forced (manual sync).
|
||||
* @return Promise resolved when done, rejected if failure.
|
||||
*/
|
||||
execute(siteId?: string, force?: boolean): Promise<void> {
|
||||
return AddonModLessonSync.instance.syncAllLessons(siteId, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time between consecutive executions.
|
||||
*
|
||||
* @return Time between consecutive executions (in ms).
|
||||
*/
|
||||
getInterval(): number {
|
||||
return AddonModLessonSync.instance.syncInterval;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class AddonModLessonSyncCronHandler extends makeSingleton(AddonModLessonSyncCronHandlerService) {}
|
|
@ -0,0 +1,518 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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 { CoreError } from '@classes/errors/error';
|
||||
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreSync } from '@services/sync';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { CoreUrlUtils } from '@services/utils/url';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
|
||||
import { AddonModLessonRetakeFinishedInSyncDBRecord, RETAKES_FINISHED_SYNC_TABLE_NAME } from './database/lesson';
|
||||
import { AddonModLessonGetPasswordResult, AddonModLessonPrefetchHandler } from './handlers/prefetch';
|
||||
import { AddonModLesson, AddonModLessonLessonWSData, AddonModLessonProvider } from './lesson';
|
||||
import { AddonModLessonOffline, AddonModLessonPageAttemptRecord } from './lesson-offline';
|
||||
|
||||
/**
|
||||
* Service to sync lesson.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonModLessonSyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModLessonSyncResult> {
|
||||
|
||||
static readonly AUTO_SYNCED = 'addon_mod_lesson_autom_synced';
|
||||
|
||||
protected componentTranslate?: string;
|
||||
|
||||
constructor() {
|
||||
super('AddonModLessonSyncProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmark a retake as finished in a synchronization.
|
||||
*
|
||||
* @param lessonId Lesson ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async deleteRetakeFinishedInSync(lessonId: number, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
// Ignore errors, maybe there is none.
|
||||
await CoreUtils.instance.ignoreErrors(site.getDb().deleteRecords(RETAKES_FINISHED_SYNC_TABLE_NAME, { lessonid: lessonId }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a retake finished in a synchronization for a certain lesson (if any).
|
||||
*
|
||||
* @param lessonId Lesson ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the retake entry (undefined if no retake).
|
||||
*/
|
||||
async getRetakeFinishedInSync(
|
||||
lessonId: number,
|
||||
siteId?: string,
|
||||
): Promise<AddonModLessonRetakeFinishedInSyncDBRecord | undefined> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
return CoreUtils.instance.ignoreErrors(site.getDb().getRecord(RETAKES_FINISHED_SYNC_TABLE_NAME, { lessonid: lessonId }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a lesson has data to synchronize.
|
||||
*
|
||||
* @param lessonId Lesson ID.
|
||||
* @param retake Retake number.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with boolean: whether it has data to sync.
|
||||
*/
|
||||
async hasDataToSync(lessonId: number, retake: number, siteId?: string): Promise<boolean> {
|
||||
|
||||
const [hasAttempts, hasFinished] = await Promise.all([
|
||||
CoreUtils.instance.ignoreErrors(AddonModLessonOffline.instance.hasRetakeAttempts(lessonId, retake, siteId)),
|
||||
CoreUtils.instance.ignoreErrors(AddonModLessonOffline.instance.hasFinishedRetake(lessonId, siteId)),
|
||||
]);
|
||||
|
||||
return !!(hasAttempts || hasFinished);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a retake as finished in a synchronization.
|
||||
*
|
||||
* @param lessonId Lesson ID.
|
||||
* @param retake The retake number.
|
||||
* @param pageId The page ID to start reviewing from.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async setRetakeFinishedInSync(lessonId: number, retake: number, pageId: number, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
await site.getDb().insertRecord(RETAKES_FINISHED_SYNC_TABLE_NAME, <AddonModLessonRetakeFinishedInSyncDBRecord> {
|
||||
lessonid: lessonId,
|
||||
retake: Number(retake),
|
||||
pageid: Number(pageId),
|
||||
timefinished: CoreTimeUtils.instance.timestamp(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to synchronize all the lessons in a certain site or in all sites.
|
||||
*
|
||||
* @param siteId Site ID to sync. If not defined, sync all sites.
|
||||
* @param force Wether to force sync not depending on last execution.
|
||||
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
syncAllLessons(siteId?: string, force?: boolean): Promise<void> {
|
||||
return this.syncOnSites('all lessons', this.syncAllLessonsFunc.bind(this, !!force), siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all lessons on a site.
|
||||
*
|
||||
* @param force Wether to force sync not depending on last execution.
|
||||
* @param siteId Site ID to sync.
|
||||
* @param Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
protected async syncAllLessonsFunc(force: boolean, siteId: string): Promise<void> {
|
||||
// Get all the lessons that have something to be synchronized.
|
||||
const lessons = await AddonModLessonOffline.instance.getAllLessonsWithData(siteId);
|
||||
|
||||
// Sync all lessons that need it.
|
||||
await Promise.all(lessons.map(async (lesson) => {
|
||||
const result = force ?
|
||||
await this.syncLesson(lesson.id, false, false, siteId) :
|
||||
await this.syncLessonIfNeeded(lesson.id, false, siteId);
|
||||
|
||||
if (result?.updated) {
|
||||
// Sync successful, send event.
|
||||
CoreEvents.trigger<AddonModLessonAutoSyncData>(AddonModLessonSyncProvider.AUTO_SYNCED, {
|
||||
lessonId: lesson.id,
|
||||
warnings: result.warnings,
|
||||
}, siteId);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a lesson only if a certain time has passed since the last time.
|
||||
*
|
||||
* @param lessonId Lesson ID.
|
||||
* @param askPreflight Whether we should ask for password if needed.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when the lesson is synced or if it doesn't need to be synced.
|
||||
*/
|
||||
async syncLessonIfNeeded(
|
||||
lessonId: number,
|
||||
askPassword?: boolean,
|
||||
siteId?: string,
|
||||
): Promise<AddonModLessonSyncResult | undefined> {
|
||||
const needed = await this.isSyncNeeded(lessonId, siteId);
|
||||
|
||||
if (needed) {
|
||||
return this.syncLesson(lessonId, askPassword, false, siteId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to synchronize a lesson.
|
||||
*
|
||||
* @param lessonId Lesson ID.
|
||||
* @param askPassword True if we should ask for password if needed, false otherwise.
|
||||
* @param ignoreBlock True to ignore the sync block setting.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved in success.
|
||||
*/
|
||||
async syncLesson(
|
||||
lessonId: number,
|
||||
askPassword?: boolean,
|
||||
ignoreBlock?: boolean,
|
||||
siteId?: string,
|
||||
): Promise<AddonModLessonSyncResult> {
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
this.componentTranslate = this.componentTranslate || CoreCourse.instance.translateModuleName('lesson');
|
||||
|
||||
let syncPromise = this.getOngoingSync(lessonId, siteId);
|
||||
if (syncPromise) {
|
||||
// There's already a sync ongoing for this lesson, return the promise.
|
||||
return syncPromise;
|
||||
}
|
||||
|
||||
// Verify that lesson isn't blocked.
|
||||
if (!ignoreBlock && CoreSync.instance.isBlocked(AddonModLessonProvider.COMPONENT, lessonId, siteId)) {
|
||||
this.logger.debug('Cannot sync lesson ' + lessonId + ' because it is blocked.');
|
||||
|
||||
throw new CoreError(Translate.instance.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
|
||||
}
|
||||
|
||||
this.logger.debug('Try to sync lesson ' + lessonId + ' in site ' + siteId);
|
||||
|
||||
syncPromise = this.performSyncLesson(lessonId, askPassword, ignoreBlock, siteId);
|
||||
|
||||
return this.addOngoingSync(lessonId, syncPromise, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to synchronize a lesson.
|
||||
*
|
||||
* @param lessonId Lesson ID.
|
||||
* @param askPassword True if we should ask for password if needed, false otherwise.
|
||||
* @param ignoreBlock True to ignore the sync block setting.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved in success.
|
||||
*/
|
||||
protected async performSyncLesson(
|
||||
lessonId: number,
|
||||
askPassword?: boolean,
|
||||
ignoreBlock?: boolean,
|
||||
siteId?: string,
|
||||
): Promise<AddonModLessonSyncResult> {
|
||||
// Sync offline logs.
|
||||
await CoreUtils.instance.ignoreErrors(
|
||||
CoreCourseLogHelper.instance.syncActivity(AddonModLessonProvider.COMPONENT, lessonId, siteId),
|
||||
);
|
||||
|
||||
const result: AddonModLessonSyncResult = {
|
||||
warnings: [],
|
||||
updated: false,
|
||||
};
|
||||
|
||||
// Try to synchronize the page attempts first.
|
||||
const passwordData = await this.syncAttempts(lessonId, result, askPassword, siteId);
|
||||
|
||||
// Now sync the retake.
|
||||
await this.syncRetake(lessonId, result, passwordData, askPassword, ignoreBlock, siteId);
|
||||
|
||||
if (result.updated && result.courseId) {
|
||||
try {
|
||||
// Data has been sent to server, update data.
|
||||
const module = await CoreCourse.instance.getModuleBasicInfoByInstance(lessonId, 'lesson', siteId);
|
||||
await this.prefetchAfterUpdate(AddonModLessonPrefetchHandler.instance, module, result.courseId, undefined, siteId);
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
// Sync finished, set sync time.
|
||||
await CoreUtils.instance.ignoreErrors(this.setSyncTime(String(lessonId), siteId));
|
||||
|
||||
// All done, return the result.
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all page attempts.
|
||||
*
|
||||
* @param lessonId Lesson ID.
|
||||
* @param result Sync result where to store the result.
|
||||
* @param askPassword True if we should ask for password if needed, false otherwise.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
*/
|
||||
protected async syncAttempts(
|
||||
lessonId: number,
|
||||
result: AddonModLessonSyncResult,
|
||||
askPassword?: boolean,
|
||||
siteId?: string,
|
||||
): Promise<AddonModLessonGetPasswordResult | undefined> {
|
||||
let attempts = await AddonModLessonOffline.instance.getLessonAttempts(lessonId, siteId);
|
||||
|
||||
if (!attempts.length) {
|
||||
return;
|
||||
} else if (!CoreApp.instance.isOnline()) {
|
||||
// Cannot sync in offline.
|
||||
throw new CoreNetworkError();
|
||||
}
|
||||
|
||||
result.courseId = attempts[0].courseid;
|
||||
const attemptsLength = attempts.length;
|
||||
|
||||
// Get the info, access info and the lesson password if needed.
|
||||
const lesson = await AddonModLesson.instance.getLessonById(result.courseId, lessonId, { siteId });
|
||||
|
||||
const passwordData = await AddonModLessonPrefetchHandler.instance.getLessonPassword(lessonId, {
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
askPassword,
|
||||
siteId,
|
||||
});
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
passwordData.lesson = passwordData.lesson || lesson;
|
||||
|
||||
// Filter the attempts, get only the ones that belong to the current retake.
|
||||
attempts = attempts.filter((attempt) => {
|
||||
if (attempt.retake == passwordData.accessInfo.attemptscount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Attempt doesn't belong to current retake, delete.
|
||||
promises.push(CoreUtils.instance.ignoreErrors(AddonModLessonOffline.instance.deleteAttempt(
|
||||
lesson.id,
|
||||
attempt.retake,
|
||||
attempt.pageid,
|
||||
attempt.timemodified,
|
||||
siteId,
|
||||
)));
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (attempts.length != attemptsLength) {
|
||||
// Some attempts won't be sent, add a warning.
|
||||
result.warnings.push(Translate.instance.instant('core.warningofflinedatadeleted', {
|
||||
component: this.componentTranslate,
|
||||
name: lesson.name,
|
||||
error: Translate.instance.instant('addon.mod_lesson.warningretakefinished'),
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (!attempts.length) {
|
||||
return passwordData;
|
||||
}
|
||||
|
||||
// Send the attempts in the same order they were answered.
|
||||
attempts.sort((a, b) => a.timemodified - b.timemodified);
|
||||
|
||||
const promisesData = attempts.map((attempt) => ({
|
||||
function: this.sendAttempt.bind(this, lesson, passwordData.password, attempt, result, siteId),
|
||||
blocking: true,
|
||||
}));
|
||||
|
||||
await CoreUtils.instance.executeOrderedPromises(promisesData);
|
||||
|
||||
return passwordData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an attempt to the site and delete it afterwards.
|
||||
*
|
||||
* @param lesson Lesson.
|
||||
* @param password Password (if any).
|
||||
* @param attempt Attempt to send.
|
||||
* @param result Result where to store the data.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async sendAttempt(
|
||||
lesson: AddonModLessonLessonWSData,
|
||||
password: string,
|
||||
attempt: AddonModLessonPageAttemptRecord,
|
||||
result: AddonModLessonSyncResult,
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
const retake = attempt.retake;
|
||||
const pageId = attempt.pageid;
|
||||
const timemodified = attempt.timemodified;
|
||||
|
||||
try {
|
||||
// Send the page data.
|
||||
await AddonModLesson.instance.processPageOnline(lesson.id, attempt.pageid, attempt.data || {}, {
|
||||
password,
|
||||
siteId,
|
||||
});
|
||||
|
||||
result.updated = true;
|
||||
|
||||
await AddonModLessonOffline.instance.deleteAttempt(lesson.id, retake, pageId, timemodified, siteId);
|
||||
} catch (error) {
|
||||
if (!error || !CoreUtils.instance.isWebServiceError(error)) {
|
||||
// Couldn't connect to server.
|
||||
throw error;
|
||||
}
|
||||
|
||||
// The WebService has thrown an error, this means that the attempt cannot be submitted. Delete it.
|
||||
result.updated = true;
|
||||
|
||||
await AddonModLessonOffline.instance.deleteAttempt(lesson.id, retake, pageId, timemodified, siteId);
|
||||
|
||||
// Attempt deleted, add a warning.
|
||||
result.warnings.push(Translate.instance.instant('core.warningofflinedatadeleted', {
|
||||
component: this.componentTranslate,
|
||||
name: lesson.name,
|
||||
error: CoreTextUtils.instance.getErrorMessageFromError(error),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync retake.
|
||||
*
|
||||
* @param lessonId Lesson ID.
|
||||
* @param result Sync result where to store the result.
|
||||
* @param passwordData Password data. If not provided it will be calculated.
|
||||
* @param askPassword True if we should ask for password if needed, false otherwise.
|
||||
* @param ignoreBlock True to ignore the sync block setting.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
*/
|
||||
protected async syncRetake(
|
||||
lessonId: number,
|
||||
result: AddonModLessonSyncResult,
|
||||
passwordData?: AddonModLessonGetPasswordResult,
|
||||
askPassword?: boolean,
|
||||
ignoreBlock?: boolean,
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
// Attempts sent or there was none. If there is a finished retake, send it.
|
||||
const retake = await CoreUtils.instance.ignoreErrors(AddonModLessonOffline.instance.getRetake(lessonId, siteId));
|
||||
|
||||
if (!retake) {
|
||||
// No retake to sync.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!retake.finished) {
|
||||
// The retake isn't marked as finished, nothing to send. Delete the retake.
|
||||
await AddonModLessonOffline.instance.deleteRetake(lessonId, siteId);
|
||||
|
||||
return;
|
||||
} else if (!CoreApp.instance.isOnline()) {
|
||||
// Cannot sync in offline.
|
||||
throw new CoreNetworkError();
|
||||
}
|
||||
|
||||
result.courseId = retake.courseid || result.courseId;
|
||||
|
||||
if (!passwordData?.lesson) {
|
||||
// Retrieve the needed data.
|
||||
const lesson = await AddonModLesson.instance.getLessonById(result.courseId!, lessonId, { siteId });
|
||||
passwordData = await AddonModLessonPrefetchHandler.instance.getLessonPassword(lessonId, {
|
||||
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||
askPassword,
|
||||
siteId,
|
||||
});
|
||||
|
||||
passwordData.lesson = passwordData.lesson || lesson;
|
||||
}
|
||||
|
||||
if (retake.retake != passwordData.accessInfo.attemptscount) {
|
||||
// The retake changed, add a warning if it isn't there already.
|
||||
if (!result.warnings.length) {
|
||||
result.warnings.push(Translate.instance.instant('core.warningofflinedatadeleted', {
|
||||
component: this.componentTranslate,
|
||||
name: passwordData.lesson.name,
|
||||
error: Translate.instance.instant('addon.mod_lesson.warningretakefinished'),
|
||||
}));
|
||||
}
|
||||
|
||||
await AddonModLessonOffline.instance.deleteRetake(lessonId, siteId);
|
||||
}
|
||||
|
||||
try {
|
||||
// All good, finish the retake.
|
||||
const response = await AddonModLesson.instance.finishRetakeOnline(lessonId, {
|
||||
password: passwordData.password,
|
||||
siteId,
|
||||
});
|
||||
|
||||
result.updated = true;
|
||||
|
||||
// Mark the retake as finished in a sync if it can be reviewed.
|
||||
if (!ignoreBlock && response.data?.reviewlesson) {
|
||||
const params = CoreUrlUtils.instance.extractUrlParams(<string> response.data.reviewlesson.value);
|
||||
if (params.pageid) {
|
||||
// The retake can be reviewed, mark it as finished. Don't block the user for this.
|
||||
this.setRetakeFinishedInSync(lessonId, retake.retake, Number(params.pageid), siteId);
|
||||
}
|
||||
}
|
||||
|
||||
await AddonModLessonOffline.instance.deleteRetake(lessonId, siteId);
|
||||
} catch (error) {
|
||||
if (!error || !CoreUtils.instance.isWebServiceError(error)) {
|
||||
// Couldn't connect to server.
|
||||
throw error;
|
||||
}
|
||||
|
||||
// The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
|
||||
result.updated = true;
|
||||
|
||||
await AddonModLessonOffline.instance.deleteRetake(lessonId, siteId);
|
||||
|
||||
// Retake deleted, add a warning.
|
||||
result.warnings.push(Translate.instance.instant('core.warningofflinedatadeleted', {
|
||||
component: this.componentTranslate,
|
||||
name: passwordData.lesson.name,
|
||||
error: CoreTextUtils.instance.getErrorMessageFromError(error),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class AddonModLessonSync extends makeSingleton(AddonModLessonSyncProvider) {}
|
||||
|
||||
/**
|
||||
* Data returned by a lesson sync.
|
||||
*/
|
||||
export type AddonModLessonSyncResult = {
|
||||
warnings: string[]; // List of warnings.
|
||||
updated: boolean; // Whether some data was sent to the server or offline data was updated.
|
||||
courseId?: number; // Course the lesson belongs to (if known).
|
||||
};
|
||||
|
||||
/**
|
||||
* Data passed to AUTO_SYNCED event.
|
||||
*/
|
||||
export type AddonModLessonAutoSyncData = CoreEventSiteData & {
|
||||
lessonId: number;
|
||||
warnings: string[];
|
||||
};
|
Loading…
Reference in New Issue