forked from CIT/Vmeda.Online
		
	MOBILE-3648 lesson: Implement sync service and prefetch handler
This commit is contained in:
		
							parent
							
								
									2ff5883026
								
							
						
					
					
						commit
						f1fbb75889
					
				
							
								
								
									
										39
									
								
								src/addons/mod/lesson/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/addons/mod/lesson/components/components.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										576
									
								
								src/addons/mod/lesson/services/handlers/prefetch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										576
									
								
								src/addons/mod/lesson/services/handlers/prefetch.ts
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										52
									
								
								src/addons/mod/lesson/services/handlers/sync-cron.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/addons/mod/lesson/services/handlers/sync-cron.ts
									
									
									
									
									
										Normal file
									
								
							@ -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) {}
 | 
			
		||||
							
								
								
									
										518
									
								
								src/addons/mod/lesson/services/lesson-sync.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										518
									
								
								src/addons/mod/lesson/services/lesson-sync.ts
									
									
									
									
									
										Normal file
									
								
							@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user